diff options
| author | YurenHao0426 <blackhao0426@gmail.com> | 2026-02-10 20:16:36 +0000 |
|---|---|---|
| committer | YurenHao0426 <blackhao0426@gmail.com> | 2026-02-10 20:16:36 +0000 |
| commit | 5626080ca4c4219aec4888d6b9406d0d3349fb55 (patch) | |
| tree | 86287d9fd5833e11ccd78566992540f2664fd195 | |
| parent | a2036838807428424bbbaff507a6563749a83145 (diff) | |
Add RAG rewrite, 60-session experiment scripts, and analysis tools
- RAG rewrite adapter and vector preference pipeline in personalized_llm
- 60-session experiment queue scripts (reflection, rag, rag_vector, rag_rewrite)
- Vector-preference correlation analysis and visualization scripts
- Local reward model batch processing improvements
- Updated CLAUDE.md with full experiment documentation and notes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
31 files changed, 3107 insertions, 219 deletions
@@ -349,3 +349,39 @@ pkill -f "vllm.entrypoints" For questions about this codebase, refer to the experiment plan at: `/u/yurenh2/.claude/plans/effervescent-mapping-ocean.md` + +--- + +## Future Improvements (To Try) + +### Retrieval Quality Improvements + +**Problem Identified**: Current retrieval uses raw user message as query (e.g., "shortest palindrome"), but this doesn't match well with preference descriptions (e.g., "break down code with explanations"). Reranker matches task content, not preference applicability. + +**Proposed Solutions**: + +#### 1. Query Transformation +Instead of using raw user message as retrieval query, construct preference-oriented queries: +- Option A: Use LLM to generate "what user preferences might apply to this task?" +- Option B: Append task-type keywords to query (e.g., "code explanation preferences for: shortest palindrome") +- Option C: Multi-query retrieval - one for task content, one for task type/category + +#### 4. Global vs Conditional Preferences +Separate preferences into two tiers: +- **Global preferences**: High-frequency, always-applicable (e.g., "always use numbered steps", "use Python for code") + - Always include in context, no retrieval needed + - Identify via frequency analysis or explicit "When general" condition +- **Conditional preferences**: Context-specific (e.g., "when debugging, focus on specific issue") + - Only these need retrieval based on task context + - Reduces retrieval burden and ensures universal preferences never missed + +**Implementation Notes**: +- Can be tested as ablation after current experiments complete +- Evaluate by: enforcement rate reduction, retrieval recall of actually-enforced preferences + +--- + +## RAG Improvement Ideas + +See [docs/rag_improvement_ideas.md](docs/rag_improvement_ideas.md) for detailed brainstorming on how to improve RAG retrieval quality and reduce timeout rate. + diff --git a/collaborativeagents/adapters/personalized_llm_adapter.py b/collaborativeagents/adapters/personalized_llm_adapter.py index b476272..488b241 100644 --- a/collaborativeagents/adapters/personalized_llm_adapter.py +++ b/collaborativeagents/adapters/personalized_llm_adapter.py @@ -36,12 +36,24 @@ class AdapterConfig: enable_rl_updates: bool = True use_user_vector: bool = True # Whether to use user vector in policy scoring - # Paths (absolute paths to actual file locations in the repo) + # Paths - computed relative to project root # Note: Using empty_store to start fresh - RAG will accumulate memories during evaluation - user_store_path: str = "/projects/bfqt/users/yurenh2/ml-projects/personalization-user-model/data/users/collab_eval_store.npz" - memory_cards_path: str = "/projects/bfqt/users/yurenh2/ml-projects/personalization-user-model/data/corpora/empty_store/memory_cards.jsonl" - memory_embeddings_path: str = "/projects/bfqt/users/yurenh2/ml-projects/personalization-user-model/data/corpora/empty_store/memory_embeddings.npy" - item_projection_path: str = "/projects/bfqt/users/yurenh2/ml-projects/personalization-user-model/data/corpora/item_projection.npz" + _project_root: str = field(default_factory=lambda: str(Path(__file__).parent.parent.parent)) + user_store_path: str = "" + memory_cards_path: str = "" + memory_embeddings_path: str = "" + item_projection_path: str = "" + + def __post_init__(self): + root = Path(self._project_root) + if not self.user_store_path: + self.user_store_path = str(root / "data/users/collab_eval_store.npz") + if not self.memory_cards_path: + self.memory_cards_path = str(root / "data/corpora/empty_store/memory_cards.jsonl") + if not self.memory_embeddings_path: + self.memory_embeddings_path = str(root / "data/corpora/empty_store/memory_embeddings.npy") + if not self.item_projection_path: + self.item_projection_path = str(root / "data/corpora/item_projection.npz") # Multi-GPU assignment device_assignment: Optional[Dict[str, str]] = None @@ -64,10 +76,30 @@ class AdapterConfig: # vLLM URL for local reward model (only used when reward_mode="llm_local") reward_vllm_url: str = "http://localhost:8005/v1" + # Retrieval optimizations + enable_query_transform: bool = False # Transform queries for better retrieval matching + enable_global_preferences: bool = False # Separate global prefs that bypass retrieval + enable_preference_rewrite: bool = False # Use LLM to rewrite/merge retrieved preferences + + # Dynamic topk settings + dynamic_topk: bool = False # Use dynamic selection based on rerank score distribution + dynamic_min_k: int = 3 # Minimum preferences to select + dynamic_max_k: int = 8 # Maximum preferences to select + dynamic_score_ratio: float = 0.5 # Threshold = top_score * ratio + + # RL learning rate overrides + eta_long: float = None # Override RL learning rate for z_long (default 0.01) + eta_short: float = None # Override RL learning rate for z_short (default 0.05) + + # Session-level preference consolidation + enable_preference_consolidation: bool = False # Consolidate preferences at session end + consolidation_threshold: int = 5 # Min preferences before consolidation kicks in + # Reward mapping for user behavior preference_enforcement_reward: float = -0.8 # Negative reward when user enforces disappointment_expression_reward: float = -0.4 # Milder negative for disappointment positive_feedback_reward: float = 0.5 # When user expresses satisfaction + no_enforcement_reward: float = 0.1 # Small positive when user doesn't enforce (good turn) task_completion_reward: float = 1.0 # When task is solved correctly @@ -120,6 +152,17 @@ class PersonalizedLLMAdapter: best_of_n=self.config.best_of_n, reward_mode=self.config.reward_mode, reward_vllm_url=self.config.reward_vllm_url, + enable_query_transform=self.config.enable_query_transform, + enable_global_preferences=self.config.enable_global_preferences, + enable_preference_rewrite=self.config.enable_preference_rewrite, + dynamic_topk=self.config.dynamic_topk, + dynamic_min_k=self.config.dynamic_min_k, + dynamic_max_k=self.config.dynamic_max_k, + dynamic_score_ratio=self.config.dynamic_score_ratio, + eta_long=self.config.eta_long, + eta_short=self.config.eta_short, + enable_preference_consolidation=self.config.enable_preference_consolidation, + consolidation_threshold=self.config.consolidation_threshold, ) self._initialized = True print("[Adapter] Initialization complete.") @@ -221,7 +264,9 @@ class PersonalizedLLMAdapter: self.initialize() # Use chat_prepare from PersonalizedLLM - result = self._llm.chat_prepare(self._current_user_id, query) + # skip_extraction=False to enable preference extraction from user messages + # skip_auto_reward=True because batch framework handles rewards via process_user_turn + result = self._llm.chat_prepare(self._current_user_id, query, skip_extraction=False, skip_auto_reward=True) return result["messages"], result["context"] def process_response( @@ -294,24 +339,38 @@ class PersonalizedLLMAdapter: This is called AFTER generate_response and BEFORE the next turn. """ # Derive reward from user behavior - reward = 0.0 - gating = 1.0 # Always apply (could be conditional) + # Key insight: ALWAYS give a reward signal, not just for enforcement + # - Enforcement: negative reward (user had to correct agent) + # - No enforcement: small positive reward (agent did well) + # - Satisfaction/progress: larger positive reward + gating = 1.0 # Always apply if enforce_preferences: - reward = self.config.preference_enforcement_reward + reward = self.config.preference_enforcement_reward # -0.8 self._session_metrics["enforcements"] += 1 self._total_enforcements += 1 elif express_disappointment: - reward = self.config.disappointment_expression_reward + reward = self.config.disappointment_expression_reward # -0.4 self._session_metrics["disappointments"] += 1 self._total_disappointments += 1 elif express_satisfaction or draft_answer_updated: - reward = self.config.positive_feedback_reward + reward = self.config.positive_feedback_reward # +0.5 - # Apply feedback to PersonalizedLLM - if self.config.enable_rl_updates and reward != 0.0: + else: + # No enforcement = good turn, give small positive reward + reward = self.config.no_enforcement_reward # +0.1 + + # Apply feedback to PersonalizedLLM (always, not just when reward != 0) + if self.config.enable_rl_updates: + # Debug: check if pending_rl_update exists + ctx = self._llm._sessions.get(self._current_user_id) + has_pending = ctx is not None and ctx.pending_rl_update is not None + has_chosen = (has_pending and + len(ctx.pending_rl_update.get("last_chosen_indices", [])) > 0) if has_pending else False + print(f"[DEBUG-RL] User={self._current_user_id} reward={reward:.2f} " + f"has_pending={has_pending} has_chosen={has_chosen}") feedback = Feedback( user_id=self._current_user_id, turn_id=self._turn_counter - 1, @@ -380,6 +439,66 @@ class PersonalizedLLMAdapter: if self._initialized: self._llm.persist() + def export_all_user_vectors(self) -> Dict[str, Dict[str, Any]]: + """ + Export all user vectors with full state for analysis. + + Returns: + Dict mapping user_id to dict containing: + - z_long: np.ndarray (long-term user vector) + - z_short: np.ndarray (short-term user vector) + - z_long_norm: float + - z_short_norm: float + - reward_ma: float (reward moving average) + """ + if not self._initialized: + return {} + + result = {} + for user_id, state in self._llm._user_store._states.items(): + result[user_id] = { + "z_long": state.z_long.tolist(), + "z_short": state.z_short.tolist(), + "z_long_norm": float(np.linalg.norm(state.z_long)), + "z_short_norm": float(np.linalg.norm(state.z_short)), + "reward_ma": float(state.reward_ma), + } + return result + + def export_user_vectors_npz(self, output_path: str) -> None: + """ + Export all user vectors to a numpy .npz file for efficient storage and analysis. + + Args: + output_path: Path to save the .npz file + + The saved file contains: + - user_ids: array of user IDs + - z_long: [n_users, k] array of long-term vectors + - z_short: [n_users, k] array of short-term vectors + - reward_ma: [n_users] array of reward moving averages + """ + if not self._initialized: + return + + states = self._llm._user_store._states + if not states: + return + + user_ids = list(states.keys()) + z_long = np.stack([states[uid].z_long for uid in user_ids]) + z_short = np.stack([states[uid].z_short for uid in user_ids]) + reward_ma = np.array([states[uid].reward_ma for uid in user_ids]) + + np.savez( + output_path, + user_ids=np.array(user_ids), + z_long=z_long, + z_short=z_short, + reward_ma=reward_ma, + ) + print(f"[Adapter] Exported {len(user_ids)} user vectors to {output_path}") + # ========================================================================= # CollaborativeAgents Interface Methods # ========================================================================= @@ -491,6 +610,45 @@ def create_baseline_adapter( use_user_vector=False, # No user vector in policy llm_name=llm_name, use_shared_models=use_shared_models, + enable_query_transform=True, + enable_global_preferences=True, + device_assignment={ + "embed": "cuda:2", + "reranker": "cuda:3", + "extractor": "cuda:2", + }, + ), + # Baseline 6b: RAG with dynamic topk (min=3, max=8, ratio=0.5) + "rag_dynamic": AdapterConfig( + mode="nopersonal", + enable_preference_extraction=True, + enable_rl_updates=False, + use_user_vector=False, + llm_name=llm_name, + use_shared_models=use_shared_models, + enable_query_transform=True, + enable_global_preferences=True, + dynamic_topk=True, + dynamic_min_k=3, + dynamic_max_k=8, + dynamic_score_ratio=0.5, + device_assignment={ + "embed": "cuda:2", + "reranker": "cuda:3", + "extractor": "cuda:2", + }, + ), + # Baseline 6c: RAG with preference rewrite (LLM merges preferences) + "rag_rewrite": AdapterConfig( + mode="nopersonal", + enable_preference_extraction=True, + enable_rl_updates=False, + use_user_vector=False, + llm_name=llm_name, + use_shared_models=use_shared_models, + enable_query_transform=True, + enable_global_preferences=True, + enable_preference_rewrite=True, # NEW: Use LLM to merge preferences device_assignment={ "embed": "cuda:2", "reranker": "cuda:3", @@ -506,6 +664,89 @@ def create_baseline_adapter( use_user_vector=True, llm_name=llm_name, use_shared_models=use_shared_models, + enable_query_transform=True, + enable_global_preferences=True, + device_assignment={ + "embed": "cuda:2", + "reranker": "cuda:3", + "extractor": "cuda:2", + }, + ), + # Baseline 7a: RAG + Vector + Preference Rewrite (combines best of both) + "rag_rewrite_vector": AdapterConfig( + mode="full", + enable_preference_extraction=True, + enable_rl_updates=True, + use_user_vector=True, + llm_name=llm_name, + use_shared_models=use_shared_models, + enable_query_transform=True, + enable_global_preferences=True, + enable_preference_rewrite=True, # LLM merges preferences + device_assignment={ + "embed": "cuda:2", + "reranker": "cuda:3", + "extractor": "cuda:2", + }, + ), + # Baseline 7b: RAG + Vector with higher learning rate (10x) + "rag_vector_fast": AdapterConfig( + mode="full", + enable_preference_extraction=True, + enable_rl_updates=True, + use_user_vector=True, + llm_name=llm_name, + use_shared_models=use_shared_models, + enable_query_transform=True, + enable_global_preferences=True, + eta_long=0.1, # 10x default (0.01) + eta_short=0.5, # 10x default (0.05) + device_assignment={ + "embed": "cuda:2", + "reranker": "cuda:3", + "extractor": "cuda:2", + }, + ), + # Baseline 7c: RAG + Vector with session-level preference consolidation + "rag_vector_consolidate": AdapterConfig( + mode="full", + enable_preference_extraction=True, + enable_rl_updates=True, + use_user_vector=True, + llm_name=llm_name, + use_shared_models=use_shared_models, + enable_query_transform=True, + enable_global_preferences=True, + enable_preference_consolidation=True, + consolidation_threshold=5, + device_assignment={ + "embed": "cuda:2", + "reranker": "cuda:3", + "extractor": "cuda:2", + }, + ), + # Baseline 7d: RAG + Vector with balanced rewards (10x LR + no_enforcement_reward) + # Key improvements: + # - 10x learning rate for faster adaptation + # - Small positive reward for turns without enforcement (+0.1) + # - Disappointment detection enabled + # - Balanced reward signal for proper REINFORCE learning + "rag_vector_balanced": AdapterConfig( + mode="full", + enable_preference_extraction=True, + enable_rl_updates=True, + use_user_vector=True, + llm_name=llm_name, + use_shared_models=use_shared_models, + enable_query_transform=True, + enable_global_preferences=True, + eta_long=0.1, # 10x default + eta_short=0.5, # 10x default + # Balanced reward structure + preference_enforcement_reward=-0.8, + disappointment_expression_reward=-0.4, + positive_feedback_reward=0.5, + no_enforcement_reward=0.1, # Key: positive signal for good turns device_assignment={ "embed": "cuda:2", "reranker": "cuda:3", diff --git a/collaborativeagents/adapters/reflection_adapter.py b/collaborativeagents/adapters/reflection_adapter.py index d535be2..451c694 100644 --- a/collaborativeagents/adapters/reflection_adapter.py +++ b/collaborativeagents/adapters/reflection_adapter.py @@ -111,6 +111,18 @@ class ReflectionAdapter: return result["content"] + def _prepend_notes_to_conversation(self, conversation, notes_text): + """Prepend notes to the first message of a conversation copy.""" + if conversation: + conversation = list(conversation) + conversation[0] = dict(conversation[0]) + conversation[0]["content"] = ( + f"Remember, you have been taking notes throughout past conversations " + f"about user preferences. Use whatever is relevant in these notes to " + f"guide your response:\n{notes_text}\n\n" + conversation[0]["content"] + ) + return conversation + def _add_scaffolding_to_conversation( self, conversation: List[Dict[str, str]], @@ -118,73 +130,73 @@ class ReflectionAdapter: ) -> List[Dict[str, str]]: """ Add scaffolding (memory notes) to conversation. - - This is the EXACT logic from CollaboratorAgent.add_scaffolding_to_conversation(). - - If with_proper_scaffolding=True: Use LLM to extract relevant notes - If with_proper_scaffolding=False: Prepend all notes to first message + Sequential version - for non-batch use only. """ if not self.with_proper_scaffolding: - # Simple mode: prepend all notes - if conversation: - conversation = list(conversation) # Copy to avoid mutation - conversation[0] = dict(conversation[0]) # Copy first message - conversation[0]["content"] = ( - f"Remember, you have been taking notes throughout past conversations " - f"about user preferences. Use whatever is relevant in these notes to " - f"guide your response:\n{agent_notes}\n\n" + conversation[0]["content"] - ) - return conversation + return self._prepend_notes_to_conversation(conversation, agent_notes) else: - # Proper scaffolding: use LLM to extract relevant notes - conversation_str = get_conversation_string(conversation) - formatted_prompt = proper_scaffolding_prompt.format( - conversation_history=conversation_str, - complete_agent_notes=agent_notes - ) - - # Call LLM to extract relevant notes (with retries) - for attempt in range(3): - try: - messages = [{"role": "user", "content": formatted_prompt}] - response = self._generate(messages, max_new_tokens=512) - - parsed = repair_json(response, return_objects=True) - missing_keys = [k for k in ["reasoning", "relevant_notes"] if k not in parsed] - - if missing_keys: - print(f"[ReflectionAdapter] Scaffolding missing keys: {missing_keys}") - continue - - scaffolded_notes = parsed["relevant_notes"] - - # Prepend extracted notes to first message - if conversation: - conversation = list(conversation) - conversation[0] = dict(conversation[0]) - conversation[0]["content"] = ( - f"Remember, you have been taking notes throughout past conversations " - f"about user preferences. Use these notes to guide your response:\n" - f"{scaffolded_notes}\n\n" + conversation[0]["content"] - ) - - return conversation + prompt = self.get_scaffolding_prompt(conversation, agent_notes) + if prompt is None: + return self._prepend_notes_to_conversation(conversation, agent_notes) + response = self._generate([{"role": "user", "content": prompt}], max_new_tokens=512) + return self.apply_scaffolding_response(conversation, agent_notes, response) + + def get_scaffolding_prompt(self, conversation, agent_notes): + """Build the scaffolding prompt for batch processing. Returns None if no scaffolding needed.""" + if not self.with_proper_scaffolding: + return None + conversation_str = get_conversation_string(conversation) + return proper_scaffolding_prompt.format( + conversation_history=conversation_str, + complete_agent_notes=agent_notes + ) - except Exception as e: - print(f"[ReflectionAdapter] Scaffolding attempt {attempt+1} failed: {e}") - continue + def apply_scaffolding_response(self, conversation, agent_notes, response): + """Apply a scaffolding LLM response to the conversation.""" + try: + parsed = repair_json(response, return_objects=True) + if "relevant_notes" in parsed: + notes_text = parsed["relevant_notes"] + if conversation: + conversation = list(conversation) + conversation[0] = dict(conversation[0]) + conversation[0]["content"] = ( + f"Remember, you have been taking notes throughout past conversations " + f"about user preferences. Use these notes to guide your response:\n" + f"{notes_text}\n\n" + conversation[0]["content"] + ) + return conversation + except Exception: + pass + # Fallback: use all notes + return self._prepend_notes_to_conversation(conversation, agent_notes) + + def get_note_update_prompt(self, user_id=None): + """Build the note-update prompt for batch processing. Returns (messages, user_id) or None.""" + uid = user_id or self._current_user_id + if not uid or not self._conversation_history: + return None + current_notes = self._user_notes.get(uid, "No notes yet.") + conversation_str = get_conversation_string(self._conversation_history) + prompt = update_agent_notes_prompt.format( + agent_notes=current_notes, + conversation_str=conversation_str + ) + return [{"role": "user", "content": prompt}] - # Fallback: use all notes if retrieval fails - print("[ReflectionAdapter] Scaffolding failed, using full notes") - if conversation: - conversation = list(conversation) - conversation[0] = dict(conversation[0]) - conversation[0]["content"] = ( - f"Remember, you have been taking notes throughout past conversations " - f"about user preferences. Use whatever is relevant in these notes to " - f"guide your response:\n{agent_notes}\n\n" + conversation[0]["content"] - ) - return conversation + def apply_note_update_response(self, response, user_id=None): + """Apply a note-update LLM response.""" + uid = user_id or self._current_user_id + if not uid: + return + try: + # 8B model: use raw response directly + updated_notes = response.strip() + if updated_notes: + old_len = len(self._user_notes.get(uid, "")) + self._user_notes[uid] = updated_notes + except Exception: + pass def start_session(self, user_id: str, user_profile: dict = None): """Start a new session for a user.""" @@ -266,11 +278,27 @@ class ReflectionAdapter: # Get current notes for this user agent_notes = self._user_notes.get(self._current_user_id, "No notes yet about this user.") - # Build conversation with scaffolding (may involve LLM call for proper_scaffolding) + # Store for batch scaffolding (prepare_prompt may be called after batch scaffolding) + self._pending_agent_notes = agent_notes + self._pending_scaffolded = False + + # Build conversation with scaffolding if self.with_scaffolding and agent_notes != "No notes yet about this user.": - conversation_with_notes = self._add_scaffolding_to_conversation( - self._conversation_history, agent_notes - ) + if hasattr(self, '_scaffolding_result') and self._scaffolding_result is not None: + # Use pre-computed batch scaffolding result + conversation_with_notes = self.apply_scaffolding_response( + list(self._conversation_history), agent_notes, self._scaffolding_result) + self._scaffolding_result = None + self._pending_scaffolded = True + elif not self.with_proper_scaffolding: + conversation_with_notes = self._prepend_notes_to_conversation( + self._conversation_history, agent_notes) + self._pending_scaffolded = True + else: + # Sequential fallback - should not happen in batch mode + conversation_with_notes = self._add_scaffolding_to_conversation( + self._conversation_history, agent_notes) + self._pending_scaffolded = True else: conversation_with_notes = self._conversation_history @@ -357,30 +385,24 @@ class ReflectionAdapter: return None # All retries failed, keep old notes - def end_session(self, task_success: bool = False) -> Dict[str, Any]: + def end_session(self, task_success: bool = False, skip_note_update: bool = False) -> Dict[str, Any]: """ End session and update agent notes via reflection. - Uses the ORIGINAL update_agent_notes logic from CollaborativeAgents. + Args: + task_success: Whether the task was completed successfully + skip_note_update: If True, skip note update (already done via batch) """ if not self._current_user_id: return {} - # Get current notes - current_notes = self._user_notes.get(self._current_user_id, "No notes yet.") - - # Update notes via session-level reflection - if len(self._conversation_history) > 0: - result = self._update_agent_notes(current_notes, self._conversation_history) - + # Update notes via session-level reflection (skip if batch already did it) + if not skip_note_update and len(self._conversation_history) > 0: + result = self._update_agent_notes( + self._user_notes.get(self._current_user_id, "No notes yet."), + self._conversation_history) if result is not None and "agent_notes" in result: - updated_notes = result["agent_notes"] - self._user_notes[self._current_user_id] = updated_notes - print(f"[ReflectionAdapter] Updated notes for {self._current_user_id} " - f"({len(current_notes)} -> {len(updated_notes)} chars)") - else: - print(f"[ReflectionAdapter] Keeping old notes for {self._current_user_id} " - f"(update failed)") + self._user_notes[self._current_user_id] = result["agent_notes"] return { "turns": len(self._conversation_history), diff --git a/collaborativeagents/adapters/reflection_grpo_adapter.py b/collaborativeagents/adapters/reflection_grpo_adapter.py index 09c5b26..3c10942 100644 --- a/collaborativeagents/adapters/reflection_grpo_adapter.py +++ b/collaborativeagents/adapters/reflection_grpo_adapter.py @@ -18,10 +18,11 @@ import torch from transformers import AutoModelForCausalLM, AutoTokenizer from json_repair import repair_json -# Model paths - Use GRPO-trained model if available, fallback to base -GRPO_MODEL_PATH = "/projects/bfqt/users/yurenh2/ml-projects/personalization-user-model/collaborativeagents/training/outputs/grpo_reflection/final" -SFT_MODEL_PATH = "/projects/bfqt/users/yurenh2/ml-projects/personalization-user-model/collaborativeagents/training/outputs/sft_reflection" -DEFAULT_MODEL_PATH = "/projects/bfqt/users/yurenh2/ml-projects/personalization-user-model/models/llama-3.1-8b-instruct" +# Model paths - computed relative to project root +_PROJECT_ROOT = Path(__file__).parent.parent.parent +GRPO_MODEL_PATH = str(_PROJECT_ROOT / "collaborativeagents/training/outputs/grpo_reflection/final") +SFT_MODEL_PATH = str(_PROJECT_ROOT / "collaborativeagents/training/outputs/sft_reflection") +DEFAULT_MODEL_PATH = str(_PROJECT_ROOT / "models/llama-3.1-8b-instruct") def get_best_available_model(): """Get the best available model path (GRPO > SFT > base).""" diff --git a/collaborativeagents/agents/local_user_agent.py b/collaborativeagents/agents/local_user_agent.py index eae311e..fd085c4 100644 --- a/collaborativeagents/agents/local_user_agent.py +++ b/collaborativeagents/agents/local_user_agent.py @@ -11,9 +11,11 @@ from typing import List, Dict, Any, Optional from copy import deepcopy from json_repair import repair_json -# Default model paths -DEFAULT_MODEL_PATH_8B = "/projects/bfqt/users/yurenh2/ml-projects/personalization-user-model/models/llama-3.1-8b-instruct" -DEFAULT_MODEL_PATH_70B = "hugging-quants/Meta-Llama-3.1-70B-Instruct-AWQ-INT4" +# Default model paths - computed relative to project root +from pathlib import Path +_PROJECT_ROOT = Path(__file__).parent.parent.parent +DEFAULT_MODEL_PATH_8B = str(_PROJECT_ROOT / "models/llama-3.1-8b-instruct") +DEFAULT_MODEL_PATH_70B = str(_PROJECT_ROOT / "models/llama-3.1-70b-instruct") # Use 70B by default for better user simulation DEFAULT_MODEL_PATH = DEFAULT_MODEL_PATH_70B diff --git a/collaborativeagents/data/complex_profiles_v2/active_profiles_8.jsonl b/collaborativeagents/data/complex_profiles_v2/active_profiles_8.jsonl new file mode 100644 index 0000000..926d290 --- /dev/null +++ b/collaborativeagents/data/complex_profiles_v2/active_profiles_8.jsonl @@ -0,0 +1,8 @@ +{"user_id": "user_4c247208", "persona": "A researcher prototyping quickly. Wants working code fast, willing to refactor later.", "preferences": [{"pref_id": "vb_001", "condition": "I say 'quick question' or 'briefly'", "action": "respond in 3 sentences or fewer, no elaboration", "conflict_group": "response_length", "priority_context": ["time_pressure", "simple_query"]}, {"pref_id": "oa_002", "condition": "teaching through code examples", "action": "break code into small chunks with explanation between each", "conflict_group": "code_presentation", "priority_context": ["teach", "learn", "understand"]}, {"pref_id": "rf_004", "condition": "teaching a new concept", "action": "build up intuition before giving the formal definition", "conflict_group": "answer_position", "priority_context": ["learning", "explain", "why"]}, {"pref_id": "cs_006", "condition": "I ask for a code review", "action": "focus only on bugs and logic errors, ignore style issues", "conflict_group": "review_scope", "priority_context": ["review", "check", "look at"]}, {"pref_id": "oa_004", "condition": "providing commands to run", "action": "use bash code blocks, include expected output as comments", "conflict_group": null, "priority_context": ["command", "run", "terminal"]}, {"pref_id": "rf_003", "condition": "providing a final answer or conclusion", "action": "put the answer first, then explanation", "conflict_group": "answer_position", "priority_context": ["direct_question", "what_is"]}, {"pref_id": "ms_005", "condition": "discussing proofs", "action": "structure as: claim, approach sketch, formal proof, intuition recap", "conflict_group": "proof_style", "priority_context": ["prove", "proof", "show that"]}, {"pref_id": "ec_004", "condition": "I ask you to correct your previous response", "action": "acknowledge the error explicitly, then provide corrected version", "conflict_group": null, "priority_context": ["you were wrong", "that's not right", "actually"]}, {"pref_id": "ds_004", "condition": "explaining a theoretical concept", "action": "start with definition, then example, then edge cases", "conflict_group": "example_position", "priority_context": ["concept", "theory", "what is"]}, {"pref_id": "cs_007", "condition": "I ask to improve or refactor code", "action": "address both logic and style, suggest modern idioms", "conflict_group": "review_scope", "priority_context": ["improve", "refactor", "better"]}, {"pref_id": "ms_001", "condition": "solving algebraic equations", "action": "show each manipulation step with the operation applied noted", "conflict_group": "math_detail", "priority_context": ["solve", "equation", "algebra"]}, {"pref_id": "ds_003", "condition": "discussing APIs or library usage", "action": "show a minimal working example before explaining parameters", "conflict_group": "example_position", "priority_context": ["api", "library", "how to use"]}, {"pref_id": "vb_003", "condition": "I ask 'why' or 'how come'", "action": "always explain the underlying reasoning, not just the what", "conflict_group": "explanation_depth", "priority_context": ["curiosity", "understanding"]}, {"pref_id": "ds_006", "condition": "writing or reviewing documentation", "action": "be concise, avoid marketing language, focus on usage", "conflict_group": null, "priority_context": ["documentation", "docs", "readme"]}, {"pref_id": "ec_001", "condition": "I make a minor error in terminology", "action": "correct it gently inline without making it a focus", "conflict_group": "correction_style", "priority_context": ["minor_error", "terminology"]}, {"pref_id": "ip_001", "condition": "the task is complex with multiple parts", "action": "confirm the plan before executing, break into phases", "conflict_group": "autonomy", "priority_context": ["complex", "multiple", "project"]}, {"pref_id": "ms_002", "condition": "discussing statistics or probability", "action": "start with intuition and real-world interpretation before formulas", "conflict_group": "math_approach", "priority_context": ["probability", "statistics", "likelihood"]}, {"pref_id": "ip_004", "condition": "I'm comparing alternatives", "action": "present trade-offs in a table format with clear criteria", "conflict_group": "guidance_style", "priority_context": ["compare", "vs", "or", "which"]}, {"pref_id": "ds_005", "condition": "discussing data structures", "action": "always include time complexity for operations mentioned", "conflict_group": null, "priority_context": ["data structure", "array", "tree", "hash"]}, {"pref_id": "vb_004", "condition": "I'm debugging and say 'it doesn't work'", "action": "focus on diagnosing the specific issue, skip general explanations", "conflict_group": "explanation_depth", "priority_context": ["debugging", "error", "fix"]}, {"pref_id": "ip_002", "condition": "I give a clear and specific instruction", "action": "execute directly without asking for confirmation", "conflict_group": "autonomy", "priority_context": ["do this", "make this", "specific_instruction"]}, {"pref_id": "cs_005", "condition": "the code is a complete module or class", "action": "use docstrings at function/class level, minimal inline comments", "conflict_group": "comment_style", "priority_context": ["module", "class", "production"]}, {"pref_id": "ds_002", "condition": "discussing system design or architecture", "action": "describe components as a list first, then explain interactions", "conflict_group": null, "priority_context": ["design", "architecture", "system"]}, {"pref_id": "oa_003", "condition": "any response with code", "action": "always specify the language in the code fence", "conflict_group": null, "priority_context": ["code"]}, {"pref_id": "ms_003", "condition": "I ask to verify my calculation", "action": "check my work step by step, point out where I diverged if wrong", "conflict_group": null, "priority_context": ["verify", "check", "is this right"]}, {"pref_id": "ec_002", "condition": "I have a fundamental misconception", "action": "address the misconception directly and clearly before proceeding", "conflict_group": "correction_style", "priority_context": ["misconception", "fundamental_error"]}, {"pref_id": "cs_003", "condition": "writing SQL queries", "action": "use UPPERCASE for keywords, lowercase for table/column names", "conflict_group": "naming_convention", "priority_context": ["sql", "database", "query"]}, {"pref_id": "ds_001", "condition": "discussing machine learning concepts", "action": "include the mathematical formulation alongside intuitive explanation", "conflict_group": null, "priority_context": ["ml", "machine learning", "model"]}, {"pref_id": "ms_004", "condition": "the problem involves calculus", "action": "state the rule being applied (chain rule, integration by parts, etc.)", "conflict_group": "math_detail", "priority_context": ["derivative", "integral", "calculus"]}, {"pref_id": "ip_005", "condition": "I express frustration or say 'this is annoying'", "action": "acknowledge the difficulty briefly, then provide direct help", "conflict_group": null, "priority_context": ["frustration", "annoying", "stuck"]}, {"pref_id": "cs_008", "condition": "providing error handling examples", "action": "always use specific exception types, never bare except", "conflict_group": null, "priority_context": ["error", "exception", "try"]}, {"pref_id": "rf_001", "condition": "listing multiple items or options", "action": "use bullet points with consistent indentation", "conflict_group": "format_structure", "priority_context": ["enumeration", "comparison"]}, {"pref_id": "cs_002", "condition": "writing JavaScript or TypeScript code", "action": "use camelCase for variables, PascalCase for classes", "conflict_group": "naming_convention", "priority_context": ["javascript", "js", "typescript", "ts"]}, {"pref_id": "rf_002", "condition": "explaining a sequential process or procedure", "action": "use numbered steps with clear transitions", "conflict_group": "format_structure", "priority_context": ["tutorial", "how-to", "setup"]}, {"pref_id": "cs_004", "condition": "the code snippet is short (under 20 lines)", "action": "include inline comments explaining each logical block", "conflict_group": "comment_style", "priority_context": ["example", "snippet"]}], "conflict_groups": {"response_length": ["vb_001"], "code_presentation": ["oa_002"], "answer_position": ["rf_004", "rf_003"], "review_scope": ["cs_006", "cs_007"], "proof_style": ["ms_005"], "example_position": ["ds_004", "ds_003"], "math_detail": ["ms_001", "ms_004"], "explanation_depth": ["vb_003", "vb_004"], "correction_style": ["ec_001", "ec_002"], "autonomy": ["ip_001", "ip_002"], "math_approach": ["ms_002"], "guidance_style": ["ip_004"], "comment_style": ["cs_005", "cs_004"], "naming_convention": ["cs_003", "cs_002"], "format_structure": ["rf_001", "rf_002"]}, "meta": {"total_preferences": 35, "total_conflict_groups": 15, "generator": "schema_based"}} +{"user_id": "user_bfdd83da", "persona": "A DevOps engineer focused on automation. Wants concise, actionable answers with commands to run.", "preferences": [{"pref_id": "cs_005", "condition": "the code is a complete module or class", "action": "use docstrings at function/class level, minimal inline comments", "conflict_group": "comment_style", "priority_context": ["module", "class", "production"]}, {"pref_id": "oa_002", "condition": "teaching through code examples", "action": "break code into small chunks with explanation between each", "conflict_group": "code_presentation", "priority_context": ["teach", "learn", "understand"]}, {"pref_id": "cs_008", "condition": "providing error handling examples", "action": "always use specific exception types, never bare except", "conflict_group": null, "priority_context": ["error", "exception", "try"]}, {"pref_id": "ds_004", "condition": "explaining a theoretical concept", "action": "start with definition, then example, then edge cases", "conflict_group": "example_position", "priority_context": ["concept", "theory", "what is"]}, {"pref_id": "ds_003", "condition": "discussing APIs or library usage", "action": "show a minimal working example before explaining parameters", "conflict_group": "example_position", "priority_context": ["api", "library", "how to use"]}, {"pref_id": "oa_003", "condition": "any response with code", "action": "always specify the language in the code fence", "conflict_group": null, "priority_context": ["code"]}, {"pref_id": "rf_004", "condition": "teaching a new concept", "action": "build up intuition before giving the formal definition", "conflict_group": "answer_position", "priority_context": ["learning", "explain", "why"]}, {"pref_id": "vb_002", "condition": "the topic involves complex algorithms or mathematics", "action": "provide detailed step-by-step derivation with intermediate results", "conflict_group": "response_length", "priority_context": ["complex_topic", "proof", "derivation"]}, {"pref_id": "ip_005", "condition": "I express frustration or say 'this is annoying'", "action": "acknowledge the difficulty briefly, then provide direct help", "conflict_group": null, "priority_context": ["frustration", "annoying", "stuck"]}, {"pref_id": "ms_001", "condition": "solving algebraic equations", "action": "show each manipulation step with the operation applied noted", "conflict_group": "math_detail", "priority_context": ["solve", "equation", "algebra"]}, {"pref_id": "rf_003", "condition": "providing a final answer or conclusion", "action": "put the answer first, then explanation", "conflict_group": "answer_position", "priority_context": ["direct_question", "what_is"]}, {"pref_id": "rf_001", "condition": "listing multiple items or options", "action": "use bullet points with consistent indentation", "conflict_group": "format_structure", "priority_context": ["enumeration", "comparison"]}, {"pref_id": "cs_002", "condition": "writing JavaScript or TypeScript code", "action": "use camelCase for variables, PascalCase for classes", "conflict_group": "naming_convention", "priority_context": ["javascript", "js", "typescript", "ts"]}, {"pref_id": "ip_002", "condition": "I give a clear and specific instruction", "action": "execute directly without asking for confirmation", "conflict_group": "autonomy", "priority_context": ["do this", "make this", "specific_instruction"]}, {"pref_id": "vb_001", "condition": "I say 'quick question' or 'briefly'", "action": "respond in 3 sentences or fewer, no elaboration", "conflict_group": "response_length", "priority_context": ["time_pressure", "simple_query"]}, {"pref_id": "ms_004", "condition": "the problem involves calculus", "action": "state the rule being applied (chain rule, integration by parts, etc.)", "conflict_group": "math_detail", "priority_context": ["derivative", "integral", "calculus"]}, {"pref_id": "vb_005", "condition": "I explicitly share my current understanding first", "action": "acknowledge what I got right, then correct only the gaps", "conflict_group": null, "priority_context": ["validation", "checking"]}, {"pref_id": "ec_001", "condition": "I make a minor error in terminology", "action": "correct it gently inline without making it a focus", "conflict_group": "correction_style", "priority_context": ["minor_error", "terminology"]}, {"pref_id": "ip_001", "condition": "the task is complex with multiple parts", "action": "confirm the plan before executing, break into phases", "conflict_group": "autonomy", "priority_context": ["complex", "multiple", "project"]}, {"pref_id": "cs_006", "condition": "I ask for a code review", "action": "focus only on bugs and logic errors, ignore style issues", "conflict_group": "review_scope", "priority_context": ["review", "check", "look at"]}, {"pref_id": "ds_005", "condition": "discussing data structures", "action": "always include time complexity for operations mentioned", "conflict_group": null, "priority_context": ["data structure", "array", "tree", "hash"]}, {"pref_id": "oa_004", "condition": "providing commands to run", "action": "use bash code blocks, include expected output as comments", "conflict_group": null, "priority_context": ["command", "run", "terminal"]}, {"pref_id": "ms_006", "condition": "I'm practicing for an exam", "action": "after solving, give a similar practice problem", "conflict_group": null, "priority_context": ["practice", "exam", "test"]}, {"pref_id": "ec_004", "condition": "I ask you to correct your previous response", "action": "acknowledge the error explicitly, then provide corrected version", "conflict_group": null, "priority_context": ["you were wrong", "that's not right", "actually"]}, {"pref_id": "ec_002", "condition": "I have a fundamental misconception", "action": "address the misconception directly and clearly before proceeding", "conflict_group": "correction_style", "priority_context": ["misconception", "fundamental_error"]}, {"pref_id": "oa_001", "condition": "generating code that will be copied", "action": "provide code in a single copyable block, no interleaved explanation", "conflict_group": "code_presentation", "priority_context": ["copy", "use this", "give me code"]}, {"pref_id": "ip_006", "condition": "I thank you or say the answer was helpful", "action": "don't add unnecessary follow-up, just acknowledge briefly", "conflict_group": null, "priority_context": ["thanks", "helpful", "great"]}, {"pref_id": "rf_002", "condition": "explaining a sequential process or procedure", "action": "use numbered steps with clear transitions", "conflict_group": "format_structure", "priority_context": ["tutorial", "how-to", "setup"]}, {"pref_id": "ms_005", "condition": "discussing proofs", "action": "structure as: claim, approach sketch, formal proof, intuition recap", "conflict_group": "proof_style", "priority_context": ["prove", "proof", "show that"]}, {"pref_id": "ip_003", "condition": "I seem uncertain or ask 'what do you think'", "action": "provide a recommendation with brief rationale, not just options", "conflict_group": "guidance_style", "priority_context": ["uncertain", "should I", "what do you think"]}, {"pref_id": "cs_001", "condition": "writing Python code", "action": "use snake_case for variables and functions, include type hints", "conflict_group": "naming_convention", "priority_context": ["python", "py"]}, {"pref_id": "ip_004", "condition": "I'm comparing alternatives", "action": "present trade-offs in a table format with clear criteria", "conflict_group": "guidance_style", "priority_context": ["compare", "vs", "or", "which"]}, {"pref_id": "ds_006", "condition": "writing or reviewing documentation", "action": "be concise, avoid marketing language, focus on usage", "conflict_group": null, "priority_context": ["documentation", "docs", "readme"]}, {"pref_id": "vb_003", "condition": "I ask 'why' or 'how come'", "action": "always explain the underlying reasoning, not just the what", "conflict_group": "explanation_depth", "priority_context": ["curiosity", "understanding"]}, {"pref_id": "ds_002", "condition": "discussing system design or architecture", "action": "describe components as a list first, then explain interactions", "conflict_group": null, "priority_context": ["design", "architecture", "system"]}, {"pref_id": "ms_003", "condition": "I ask to verify my calculation", "action": "check my work step by step, point out where I diverged if wrong", "conflict_group": null, "priority_context": ["verify", "check", "is this right"]}, {"pref_id": "cs_003", "condition": "writing SQL queries", "action": "use UPPERCASE for keywords, lowercase for table/column names", "conflict_group": "naming_convention", "priority_context": ["sql", "database", "query"]}, {"pref_id": "cs_004", "condition": "the code snippet is short (under 20 lines)", "action": "include inline comments explaining each logical block", "conflict_group": "comment_style", "priority_context": ["example", "snippet"]}, {"pref_id": "vb_004", "condition": "I'm debugging and say 'it doesn't work'", "action": "focus on diagnosing the specific issue, skip general explanations", "conflict_group": "explanation_depth", "priority_context": ["debugging", "error", "fix"]}, {"pref_id": "ds_001", "condition": "discussing machine learning concepts", "action": "include the mathematical formulation alongside intuitive explanation", "conflict_group": null, "priority_context": ["ml", "machine learning", "model"]}], "conflict_groups": {"comment_style": ["cs_005", "cs_004"], "code_presentation": ["oa_002", "oa_001"], "example_position": ["ds_004", "ds_003"], "answer_position": ["rf_004", "rf_003"], "response_length": ["vb_002", "vb_001"], "math_detail": ["ms_001", "ms_004"], "format_structure": ["rf_001", "rf_002"], "naming_convention": ["cs_002", "cs_001", "cs_003"], "autonomy": ["ip_002", "ip_001"], "correction_style": ["ec_001", "ec_002"], "review_scope": ["cs_006"], "proof_style": ["ms_005"], "guidance_style": ["ip_003", "ip_004"], "explanation_depth": ["vb_003", "vb_004"]}, "meta": {"total_preferences": 40, "total_conflict_groups": 14, "generator": "schema_based"}} +{"user_id": "user_5d82ca72", "persona": "A data scientist who thinks visually. Prefers intuition before formulas and lots of examples.", "preferences": [{"pref_id": "ec_003", "condition": "my code has a bug", "action": "show the bug location, explain why it's wrong, provide the fix", "conflict_group": null, "priority_context": ["bug", "error", "wrong"]}, {"pref_id": "ip_001", "condition": "the task is complex with multiple parts", "action": "confirm the plan before executing, break into phases", "conflict_group": "autonomy", "priority_context": ["complex", "multiple", "project"]}, {"pref_id": "ds_002", "condition": "discussing system design or architecture", "action": "describe components as a list first, then explain interactions", "conflict_group": null, "priority_context": ["design", "architecture", "system"]}, {"pref_id": "ec_001", "condition": "I make a minor error in terminology", "action": "correct it gently inline without making it a focus", "conflict_group": "correction_style", "priority_context": ["minor_error", "terminology"]}, {"pref_id": "ds_005", "condition": "discussing data structures", "action": "always include time complexity for operations mentioned", "conflict_group": null, "priority_context": ["data structure", "array", "tree", "hash"]}, {"pref_id": "ms_006", "condition": "I'm practicing for an exam", "action": "after solving, give a similar practice problem", "conflict_group": null, "priority_context": ["practice", "exam", "test"]}, {"pref_id": "ip_005", "condition": "I express frustration or say 'this is annoying'", "action": "acknowledge the difficulty briefly, then provide direct help", "conflict_group": null, "priority_context": ["frustration", "annoying", "stuck"]}, {"pref_id": "oa_001", "condition": "generating code that will be copied", "action": "provide code in a single copyable block, no interleaved explanation", "conflict_group": "code_presentation", "priority_context": ["copy", "use this", "give me code"]}, {"pref_id": "ms_005", "condition": "discussing proofs", "action": "structure as: claim, approach sketch, formal proof, intuition recap", "conflict_group": "proof_style", "priority_context": ["prove", "proof", "show that"]}, {"pref_id": "oa_004", "condition": "providing commands to run", "action": "use bash code blocks, include expected output as comments", "conflict_group": null, "priority_context": ["command", "run", "terminal"]}, {"pref_id": "ds_001", "condition": "discussing machine learning concepts", "action": "include the mathematical formulation alongside intuitive explanation", "conflict_group": null, "priority_context": ["ml", "machine learning", "model"]}, {"pref_id": "ms_001", "condition": "solving algebraic equations", "action": "show each manipulation step with the operation applied noted", "conflict_group": "math_detail", "priority_context": ["solve", "equation", "algebra"]}, {"pref_id": "cs_001", "condition": "writing Python code", "action": "use snake_case for variables and functions, include type hints", "conflict_group": "naming_convention", "priority_context": ["python", "py"]}, {"pref_id": "vb_005", "condition": "I explicitly share my current understanding first", "action": "acknowledge what I got right, then correct only the gaps", "conflict_group": null, "priority_context": ["validation", "checking"]}, {"pref_id": "vb_004", "condition": "I'm debugging and say 'it doesn't work'", "action": "focus on diagnosing the specific issue, skip general explanations", "conflict_group": "explanation_depth", "priority_context": ["debugging", "error", "fix"]}, {"pref_id": "rf_004", "condition": "teaching a new concept", "action": "build up intuition before giving the formal definition", "conflict_group": "answer_position", "priority_context": ["learning", "explain", "why"]}, {"pref_id": "oa_003", "condition": "any response with code", "action": "always specify the language in the code fence", "conflict_group": null, "priority_context": ["code"]}, {"pref_id": "vb_003", "condition": "I ask 'why' or 'how come'", "action": "always explain the underlying reasoning, not just the what", "conflict_group": "explanation_depth", "priority_context": ["curiosity", "understanding"]}, {"pref_id": "cs_002", "condition": "writing JavaScript or TypeScript code", "action": "use camelCase for variables, PascalCase for classes", "conflict_group": "naming_convention", "priority_context": ["javascript", "js", "typescript", "ts"]}, {"pref_id": "ec_002", "condition": "I have a fundamental misconception", "action": "address the misconception directly and clearly before proceeding", "conflict_group": "correction_style", "priority_context": ["misconception", "fundamental_error"]}, {"pref_id": "ds_003", "condition": "discussing APIs or library usage", "action": "show a minimal working example before explaining parameters", "conflict_group": "example_position", "priority_context": ["api", "library", "how to use"]}, {"pref_id": "vb_002", "condition": "the topic involves complex algorithms or mathematics", "action": "provide detailed step-by-step derivation with intermediate results", "conflict_group": "response_length", "priority_context": ["complex_topic", "proof", "derivation"]}, {"pref_id": "ip_003", "condition": "I seem uncertain or ask 'what do you think'", "action": "provide a recommendation with brief rationale, not just options", "conflict_group": "guidance_style", "priority_context": ["uncertain", "should I", "what do you think"]}, {"pref_id": "ms_004", "condition": "the problem involves calculus", "action": "state the rule being applied (chain rule, integration by parts, etc.)", "conflict_group": "math_detail", "priority_context": ["derivative", "integral", "calculus"]}, {"pref_id": "cs_007", "condition": "I ask to improve or refactor code", "action": "address both logic and style, suggest modern idioms", "conflict_group": "review_scope", "priority_context": ["improve", "refactor", "better"]}, {"pref_id": "ds_004", "condition": "explaining a theoretical concept", "action": "start with definition, then example, then edge cases", "conflict_group": "example_position", "priority_context": ["concept", "theory", "what is"]}, {"pref_id": "cs_008", "condition": "providing error handling examples", "action": "always use specific exception types, never bare except", "conflict_group": null, "priority_context": ["error", "exception", "try"]}, {"pref_id": "oa_002", "condition": "teaching through code examples", "action": "break code into small chunks with explanation between each", "conflict_group": "code_presentation", "priority_context": ["teach", "learn", "understand"]}, {"pref_id": "rf_002", "condition": "explaining a sequential process or procedure", "action": "use numbered steps with clear transitions", "conflict_group": "format_structure", "priority_context": ["tutorial", "how-to", "setup"]}, {"pref_id": "cs_005", "condition": "the code is a complete module or class", "action": "use docstrings at function/class level, minimal inline comments", "conflict_group": "comment_style", "priority_context": ["module", "class", "production"]}, {"pref_id": "ip_004", "condition": "I'm comparing alternatives", "action": "present trade-offs in a table format with clear criteria", "conflict_group": "guidance_style", "priority_context": ["compare", "vs", "or", "which"]}, {"pref_id": "vb_001", "condition": "I say 'quick question' or 'briefly'", "action": "respond in 3 sentences or fewer, no elaboration", "conflict_group": "response_length", "priority_context": ["time_pressure", "simple_query"]}, {"pref_id": "cs_004", "condition": "the code snippet is short (under 20 lines)", "action": "include inline comments explaining each logical block", "conflict_group": "comment_style", "priority_context": ["example", "snippet"]}, {"pref_id": "ds_006", "condition": "writing or reviewing documentation", "action": "be concise, avoid marketing language, focus on usage", "conflict_group": null, "priority_context": ["documentation", "docs", "readme"]}, {"pref_id": "rf_003", "condition": "providing a final answer or conclusion", "action": "put the answer first, then explanation", "conflict_group": "answer_position", "priority_context": ["direct_question", "what_is"]}, {"pref_id": "cs_003", "condition": "writing SQL queries", "action": "use UPPERCASE for keywords, lowercase for table/column names", "conflict_group": "naming_convention", "priority_context": ["sql", "database", "query"]}, {"pref_id": "rf_001", "condition": "listing multiple items or options", "action": "use bullet points with consistent indentation", "conflict_group": "format_structure", "priority_context": ["enumeration", "comparison"]}, {"pref_id": "ms_002", "condition": "discussing statistics or probability", "action": "start with intuition and real-world interpretation before formulas", "conflict_group": "math_approach", "priority_context": ["probability", "statistics", "likelihood"]}, {"pref_id": "ip_006", "condition": "I thank you or say the answer was helpful", "action": "don't add unnecessary follow-up, just acknowledge briefly", "conflict_group": null, "priority_context": ["thanks", "helpful", "great"]}, {"pref_id": "cs_006", "condition": "I ask for a code review", "action": "focus only on bugs and logic errors, ignore style issues", "conflict_group": "review_scope", "priority_context": ["review", "check", "look at"]}, {"pref_id": "ip_002", "condition": "I give a clear and specific instruction", "action": "execute directly without asking for confirmation", "conflict_group": "autonomy", "priority_context": ["do this", "make this", "specific_instruction"]}, {"pref_id": "ms_003", "condition": "I ask to verify my calculation", "action": "check my work step by step, point out where I diverged if wrong", "conflict_group": null, "priority_context": ["verify", "check", "is this right"]}, {"pref_id": "ec_004", "condition": "I ask you to correct your previous response", "action": "acknowledge the error explicitly, then provide corrected version", "conflict_group": null, "priority_context": ["you were wrong", "that's not right", "actually"]}], "conflict_groups": {"autonomy": ["ip_001", "ip_002"], "correction_style": ["ec_001", "ec_002"], "code_presentation": ["oa_001", "oa_002"], "proof_style": ["ms_005"], "math_detail": ["ms_001", "ms_004"], "naming_convention": ["cs_001", "cs_002", "cs_003"], "explanation_depth": ["vb_004", "vb_003"], "answer_position": ["rf_004", "rf_003"], "example_position": ["ds_003", "ds_004"], "response_length": ["vb_002", "vb_001"], "guidance_style": ["ip_003", "ip_004"], "review_scope": ["cs_007", "cs_006"], "format_structure": ["rf_002", "rf_001"], "comment_style": ["cs_005", "cs_004"], "math_approach": ["ms_002"]}, "meta": {"total_preferences": 43, "total_conflict_groups": 15, "generator": "schema_based"}} +{"user_id": "user_638d3467", "persona": "A student preparing for technical interviews. Needs step-by-step problem solving practice.", "preferences": [{"pref_id": "ip_001", "condition": "the task is complex with multiple parts", "action": "confirm the plan before executing, break into phases", "conflict_group": "autonomy", "priority_context": ["complex", "multiple", "project"]}, {"pref_id": "ip_005", "condition": "I express frustration or say 'this is annoying'", "action": "acknowledge the difficulty briefly, then provide direct help", "conflict_group": null, "priority_context": ["frustration", "annoying", "stuck"]}, {"pref_id": "cs_001", "condition": "writing Python code", "action": "use snake_case for variables and functions, include type hints", "conflict_group": "naming_convention", "priority_context": ["python", "py"]}, {"pref_id": "cs_007", "condition": "I ask to improve or refactor code", "action": "address both logic and style, suggest modern idioms", "conflict_group": "review_scope", "priority_context": ["improve", "refactor", "better"]}, {"pref_id": "ds_005", "condition": "discussing data structures", "action": "always include time complexity for operations mentioned", "conflict_group": null, "priority_context": ["data structure", "array", "tree", "hash"]}, {"pref_id": "ip_004", "condition": "I'm comparing alternatives", "action": "present trade-offs in a table format with clear criteria", "conflict_group": "guidance_style", "priority_context": ["compare", "vs", "or", "which"]}, {"pref_id": "ec_002", "condition": "I have a fundamental misconception", "action": "address the misconception directly and clearly before proceeding", "conflict_group": "correction_style", "priority_context": ["misconception", "fundamental_error"]}, {"pref_id": "cs_003", "condition": "writing SQL queries", "action": "use UPPERCASE for keywords, lowercase for table/column names", "conflict_group": "naming_convention", "priority_context": ["sql", "database", "query"]}, {"pref_id": "cs_002", "condition": "writing JavaScript or TypeScript code", "action": "use camelCase for variables, PascalCase for classes", "conflict_group": "naming_convention", "priority_context": ["javascript", "js", "typescript", "ts"]}, {"pref_id": "ec_001", "condition": "I make a minor error in terminology", "action": "correct it gently inline without making it a focus", "conflict_group": "correction_style", "priority_context": ["minor_error", "terminology"]}, {"pref_id": "vb_002", "condition": "the topic involves complex algorithms or mathematics", "action": "provide detailed step-by-step derivation with intermediate results", "conflict_group": "response_length", "priority_context": ["complex_topic", "proof", "derivation"]}, {"pref_id": "ip_002", "condition": "I give a clear and specific instruction", "action": "execute directly without asking for confirmation", "conflict_group": "autonomy", "priority_context": ["do this", "make this", "specific_instruction"]}, {"pref_id": "ms_003", "condition": "I ask to verify my calculation", "action": "check my work step by step, point out where I diverged if wrong", "conflict_group": null, "priority_context": ["verify", "check", "is this right"]}, {"pref_id": "ms_005", "condition": "discussing proofs", "action": "structure as: claim, approach sketch, formal proof, intuition recap", "conflict_group": "proof_style", "priority_context": ["prove", "proof", "show that"]}, {"pref_id": "vb_004", "condition": "I'm debugging and say 'it doesn't work'", "action": "focus on diagnosing the specific issue, skip general explanations", "conflict_group": "explanation_depth", "priority_context": ["debugging", "error", "fix"]}, {"pref_id": "oa_001", "condition": "generating code that will be copied", "action": "provide code in a single copyable block, no interleaved explanation", "conflict_group": "code_presentation", "priority_context": ["copy", "use this", "give me code"]}, {"pref_id": "ms_002", "condition": "discussing statistics or probability", "action": "start with intuition and real-world interpretation before formulas", "conflict_group": "math_approach", "priority_context": ["probability", "statistics", "likelihood"]}, {"pref_id": "vb_001", "condition": "I say 'quick question' or 'briefly'", "action": "respond in 3 sentences or fewer, no elaboration", "conflict_group": "response_length", "priority_context": ["time_pressure", "simple_query"]}, {"pref_id": "ip_006", "condition": "I thank you or say the answer was helpful", "action": "don't add unnecessary follow-up, just acknowledge briefly", "conflict_group": null, "priority_context": ["thanks", "helpful", "great"]}, {"pref_id": "cs_006", "condition": "I ask for a code review", "action": "focus only on bugs and logic errors, ignore style issues", "conflict_group": "review_scope", "priority_context": ["review", "check", "look at"]}, {"pref_id": "ms_004", "condition": "the problem involves calculus", "action": "state the rule being applied (chain rule, integration by parts, etc.)", "conflict_group": "math_detail", "priority_context": ["derivative", "integral", "calculus"]}, {"pref_id": "vb_005", "condition": "I explicitly share my current understanding first", "action": "acknowledge what I got right, then correct only the gaps", "conflict_group": null, "priority_context": ["validation", "checking"]}, {"pref_id": "oa_004", "condition": "providing commands to run", "action": "use bash code blocks, include expected output as comments", "conflict_group": null, "priority_context": ["command", "run", "terminal"]}, {"pref_id": "ds_001", "condition": "discussing machine learning concepts", "action": "include the mathematical formulation alongside intuitive explanation", "conflict_group": null, "priority_context": ["ml", "machine learning", "model"]}, {"pref_id": "rf_001", "condition": "listing multiple items or options", "action": "use bullet points with consistent indentation", "conflict_group": "format_structure", "priority_context": ["enumeration", "comparison"]}, {"pref_id": "ms_006", "condition": "I'm practicing for an exam", "action": "after solving, give a similar practice problem", "conflict_group": null, "priority_context": ["practice", "exam", "test"]}, {"pref_id": "oa_002", "condition": "teaching through code examples", "action": "break code into small chunks with explanation between each", "conflict_group": "code_presentation", "priority_context": ["teach", "learn", "understand"]}, {"pref_id": "ds_004", "condition": "explaining a theoretical concept", "action": "start with definition, then example, then edge cases", "conflict_group": "example_position", "priority_context": ["concept", "theory", "what is"]}, {"pref_id": "rf_002", "condition": "explaining a sequential process or procedure", "action": "use numbered steps with clear transitions", "conflict_group": "format_structure", "priority_context": ["tutorial", "how-to", "setup"]}, {"pref_id": "ds_003", "condition": "discussing APIs or library usage", "action": "show a minimal working example before explaining parameters", "conflict_group": "example_position", "priority_context": ["api", "library", "how to use"]}, {"pref_id": "oa_003", "condition": "any response with code", "action": "always specify the language in the code fence", "conflict_group": null, "priority_context": ["code"]}, {"pref_id": "cs_008", "condition": "providing error handling examples", "action": "always use specific exception types, never bare except", "conflict_group": null, "priority_context": ["error", "exception", "try"]}, {"pref_id": "ip_003", "condition": "I seem uncertain or ask 'what do you think'", "action": "provide a recommendation with brief rationale, not just options", "conflict_group": "guidance_style", "priority_context": ["uncertain", "should I", "what do you think"]}, {"pref_id": "vb_003", "condition": "I ask 'why' or 'how come'", "action": "always explain the underlying reasoning, not just the what", "conflict_group": "explanation_depth", "priority_context": ["curiosity", "understanding"]}, {"pref_id": "ds_006", "condition": "writing or reviewing documentation", "action": "be concise, avoid marketing language, focus on usage", "conflict_group": null, "priority_context": ["documentation", "docs", "readme"]}, {"pref_id": "cs_005", "condition": "the code is a complete module or class", "action": "use docstrings at function/class level, minimal inline comments", "conflict_group": "comment_style", "priority_context": ["module", "class", "production"]}, {"pref_id": "ec_004", "condition": "I ask you to correct your previous response", "action": "acknowledge the error explicitly, then provide corrected version", "conflict_group": null, "priority_context": ["you were wrong", "that's not right", "actually"]}, {"pref_id": "rf_003", "condition": "providing a final answer or conclusion", "action": "put the answer first, then explanation", "conflict_group": "answer_position", "priority_context": ["direct_question", "what_is"]}, {"pref_id": "ms_001", "condition": "solving algebraic equations", "action": "show each manipulation step with the operation applied noted", "conflict_group": "math_detail", "priority_context": ["solve", "equation", "algebra"]}, {"pref_id": "rf_004", "condition": "teaching a new concept", "action": "build up intuition before giving the formal definition", "conflict_group": "answer_position", "priority_context": ["learning", "explain", "why"]}], "conflict_groups": {"autonomy": ["ip_001", "ip_002"], "naming_convention": ["cs_001", "cs_003", "cs_002"], "review_scope": ["cs_007", "cs_006"], "guidance_style": ["ip_004", "ip_003"], "correction_style": ["ec_002", "ec_001"], "response_length": ["vb_002", "vb_001"], "proof_style": ["ms_005"], "explanation_depth": ["vb_004", "vb_003"], "code_presentation": ["oa_001", "oa_002"], "math_approach": ["ms_002"], "math_detail": ["ms_004", "ms_001"], "format_structure": ["rf_001", "rf_002"], "example_position": ["ds_004", "ds_003"], "comment_style": ["cs_005"], "answer_position": ["rf_003", "rf_004"]}, "meta": {"total_preferences": 40, "total_conflict_groups": 15, "generator": "schema_based"}} +{"user_id": "user_7203131a", "persona": "A data scientist who thinks visually. Prefers intuition before formulas and lots of examples.", "preferences": [{"pref_id": "cs_007", "condition": "I ask to improve or refactor code", "action": "address both logic and style, suggest modern idioms", "conflict_group": "review_scope", "priority_context": ["improve", "refactor", "better"]}, {"pref_id": "ip_002", "condition": "I give a clear and specific instruction", "action": "execute directly without asking for confirmation", "conflict_group": "autonomy", "priority_context": ["do this", "make this", "specific_instruction"]}, {"pref_id": "ec_002", "condition": "I have a fundamental misconception", "action": "address the misconception directly and clearly before proceeding", "conflict_group": "correction_style", "priority_context": ["misconception", "fundamental_error"]}, {"pref_id": "ms_006", "condition": "I'm practicing for an exam", "action": "after solving, give a similar practice problem", "conflict_group": null, "priority_context": ["practice", "exam", "test"]}, {"pref_id": "oa_002", "condition": "teaching through code examples", "action": "break code into small chunks with explanation between each", "conflict_group": "code_presentation", "priority_context": ["teach", "learn", "understand"]}, {"pref_id": "ms_002", "condition": "discussing statistics or probability", "action": "start with intuition and real-world interpretation before formulas", "conflict_group": "math_approach", "priority_context": ["probability", "statistics", "likelihood"]}, {"pref_id": "ec_003", "condition": "my code has a bug", "action": "show the bug location, explain why it's wrong, provide the fix", "conflict_group": null, "priority_context": ["bug", "error", "wrong"]}, {"pref_id": "rf_002", "condition": "explaining a sequential process or procedure", "action": "use numbered steps with clear transitions", "conflict_group": "format_structure", "priority_context": ["tutorial", "how-to", "setup"]}, {"pref_id": "ip_003", "condition": "I seem uncertain or ask 'what do you think'", "action": "provide a recommendation with brief rationale, not just options", "conflict_group": "guidance_style", "priority_context": ["uncertain", "should I", "what do you think"]}, {"pref_id": "ms_001", "condition": "solving algebraic equations", "action": "show each manipulation step with the operation applied noted", "conflict_group": "math_detail", "priority_context": ["solve", "equation", "algebra"]}, {"pref_id": "rf_001", "condition": "listing multiple items or options", "action": "use bullet points with consistent indentation", "conflict_group": "format_structure", "priority_context": ["enumeration", "comparison"]}, {"pref_id": "rf_004", "condition": "teaching a new concept", "action": "build up intuition before giving the formal definition", "conflict_group": "answer_position", "priority_context": ["learning", "explain", "why"]}, {"pref_id": "ds_001", "condition": "discussing machine learning concepts", "action": "include the mathematical formulation alongside intuitive explanation", "conflict_group": null, "priority_context": ["ml", "machine learning", "model"]}, {"pref_id": "ms_003", "condition": "I ask to verify my calculation", "action": "check my work step by step, point out where I diverged if wrong", "conflict_group": null, "priority_context": ["verify", "check", "is this right"]}, {"pref_id": "ip_001", "condition": "the task is complex with multiple parts", "action": "confirm the plan before executing, break into phases", "conflict_group": "autonomy", "priority_context": ["complex", "multiple", "project"]}, {"pref_id": "oa_004", "condition": "providing commands to run", "action": "use bash code blocks, include expected output as comments", "conflict_group": null, "priority_context": ["command", "run", "terminal"]}, {"pref_id": "ip_004", "condition": "I'm comparing alternatives", "action": "present trade-offs in a table format with clear criteria", "conflict_group": "guidance_style", "priority_context": ["compare", "vs", "or", "which"]}, {"pref_id": "cs_001", "condition": "writing Python code", "action": "use snake_case for variables and functions, include type hints", "conflict_group": "naming_convention", "priority_context": ["python", "py"]}, {"pref_id": "oa_003", "condition": "any response with code", "action": "always specify the language in the code fence", "conflict_group": null, "priority_context": ["code"]}, {"pref_id": "vb_004", "condition": "I'm debugging and say 'it doesn't work'", "action": "focus on diagnosing the specific issue, skip general explanations", "conflict_group": "explanation_depth", "priority_context": ["debugging", "error", "fix"]}, {"pref_id": "ds_002", "condition": "discussing system design or architecture", "action": "describe components as a list first, then explain interactions", "conflict_group": null, "priority_context": ["design", "architecture", "system"]}, {"pref_id": "cs_003", "condition": "writing SQL queries", "action": "use UPPERCASE for keywords, lowercase for table/column names", "conflict_group": "naming_convention", "priority_context": ["sql", "database", "query"]}, {"pref_id": "ip_006", "condition": "I thank you or say the answer was helpful", "action": "don't add unnecessary follow-up, just acknowledge briefly", "conflict_group": null, "priority_context": ["thanks", "helpful", "great"]}, {"pref_id": "vb_003", "condition": "I ask 'why' or 'how come'", "action": "always explain the underlying reasoning, not just the what", "conflict_group": "explanation_depth", "priority_context": ["curiosity", "understanding"]}, {"pref_id": "vb_005", "condition": "I explicitly share my current understanding first", "action": "acknowledge what I got right, then correct only the gaps", "conflict_group": null, "priority_context": ["validation", "checking"]}, {"pref_id": "vb_001", "condition": "I say 'quick question' or 'briefly'", "action": "respond in 3 sentences or fewer, no elaboration", "conflict_group": "response_length", "priority_context": ["time_pressure", "simple_query"]}, {"pref_id": "ds_003", "condition": "discussing APIs or library usage", "action": "show a minimal working example before explaining parameters", "conflict_group": "example_position", "priority_context": ["api", "library", "how to use"]}, {"pref_id": "ec_001", "condition": "I make a minor error in terminology", "action": "correct it gently inline without making it a focus", "conflict_group": "correction_style", "priority_context": ["minor_error", "terminology"]}, {"pref_id": "ds_005", "condition": "discussing data structures", "action": "always include time complexity for operations mentioned", "conflict_group": null, "priority_context": ["data structure", "array", "tree", "hash"]}, {"pref_id": "cs_004", "condition": "the code snippet is short (under 20 lines)", "action": "include inline comments explaining each logical block", "conflict_group": "comment_style", "priority_context": ["example", "snippet"]}, {"pref_id": "ms_004", "condition": "the problem involves calculus", "action": "state the rule being applied (chain rule, integration by parts, etc.)", "conflict_group": "math_detail", "priority_context": ["derivative", "integral", "calculus"]}, {"pref_id": "ds_004", "condition": "explaining a theoretical concept", "action": "start with definition, then example, then edge cases", "conflict_group": "example_position", "priority_context": ["concept", "theory", "what is"]}, {"pref_id": "ds_006", "condition": "writing or reviewing documentation", "action": "be concise, avoid marketing language, focus on usage", "conflict_group": null, "priority_context": ["documentation", "docs", "readme"]}, {"pref_id": "vb_002", "condition": "the topic involves complex algorithms or mathematics", "action": "provide detailed step-by-step derivation with intermediate results", "conflict_group": "response_length", "priority_context": ["complex_topic", "proof", "derivation"]}, {"pref_id": "rf_003", "condition": "providing a final answer or conclusion", "action": "put the answer first, then explanation", "conflict_group": "answer_position", "priority_context": ["direct_question", "what_is"]}, {"pref_id": "ip_005", "condition": "I express frustration or say 'this is annoying'", "action": "acknowledge the difficulty briefly, then provide direct help", "conflict_group": null, "priority_context": ["frustration", "annoying", "stuck"]}], "conflict_groups": {"review_scope": ["cs_007"], "autonomy": ["ip_002", "ip_001"], "correction_style": ["ec_002", "ec_001"], "code_presentation": ["oa_002"], "math_approach": ["ms_002"], "format_structure": ["rf_002", "rf_001"], "guidance_style": ["ip_003", "ip_004"], "math_detail": ["ms_001", "ms_004"], "answer_position": ["rf_004", "rf_003"], "naming_convention": ["cs_001", "cs_003"], "explanation_depth": ["vb_004", "vb_003"], "response_length": ["vb_001", "vb_002"], "example_position": ["ds_003", "ds_004"], "comment_style": ["cs_004"]}, "meta": {"total_preferences": 36, "total_conflict_groups": 14, "generator": "schema_based"}} +{"user_id": "user_f0694c1d", "persona": "A tech lead reviewing code from their team. Focuses on maintainability and best practices.", "preferences": [{"pref_id": "rf_002", "condition": "explaining a sequential process or procedure", "action": "use numbered steps with clear transitions", "conflict_group": "format_structure", "priority_context": ["tutorial", "how-to", "setup"]}, {"pref_id": "oa_001", "condition": "generating code that will be copied", "action": "provide code in a single copyable block, no interleaved explanation", "conflict_group": "code_presentation", "priority_context": ["copy", "use this", "give me code"]}, {"pref_id": "ec_001", "condition": "I make a minor error in terminology", "action": "correct it gently inline without making it a focus", "conflict_group": "correction_style", "priority_context": ["minor_error", "terminology"]}, {"pref_id": "ms_004", "condition": "the problem involves calculus", "action": "state the rule being applied (chain rule, integration by parts, etc.)", "conflict_group": "math_detail", "priority_context": ["derivative", "integral", "calculus"]}, {"pref_id": "cs_006", "condition": "I ask for a code review", "action": "focus only on bugs and logic errors, ignore style issues", "conflict_group": "review_scope", "priority_context": ["review", "check", "look at"]}, {"pref_id": "vb_001", "condition": "I say 'quick question' or 'briefly'", "action": "respond in 3 sentences or fewer, no elaboration", "conflict_group": "response_length", "priority_context": ["time_pressure", "simple_query"]}, {"pref_id": "ds_001", "condition": "discussing machine learning concepts", "action": "include the mathematical formulation alongside intuitive explanation", "conflict_group": null, "priority_context": ["ml", "machine learning", "model"]}, {"pref_id": "ms_003", "condition": "I ask to verify my calculation", "action": "check my work step by step, point out where I diverged if wrong", "conflict_group": null, "priority_context": ["verify", "check", "is this right"]}, {"pref_id": "ip_004", "condition": "I'm comparing alternatives", "action": "present trade-offs in a table format with clear criteria", "conflict_group": "guidance_style", "priority_context": ["compare", "vs", "or", "which"]}, {"pref_id": "vb_004", "condition": "I'm debugging and say 'it doesn't work'", "action": "focus on diagnosing the specific issue, skip general explanations", "conflict_group": "explanation_depth", "priority_context": ["debugging", "error", "fix"]}, {"pref_id": "vb_005", "condition": "I explicitly share my current understanding first", "action": "acknowledge what I got right, then correct only the gaps", "conflict_group": null, "priority_context": ["validation", "checking"]}, {"pref_id": "rf_003", "condition": "providing a final answer or conclusion", "action": "put the answer first, then explanation", "conflict_group": "answer_position", "priority_context": ["direct_question", "what_is"]}, {"pref_id": "ds_002", "condition": "discussing system design or architecture", "action": "describe components as a list first, then explain interactions", "conflict_group": null, "priority_context": ["design", "architecture", "system"]}, {"pref_id": "oa_004", "condition": "providing commands to run", "action": "use bash code blocks, include expected output as comments", "conflict_group": null, "priority_context": ["command", "run", "terminal"]}, {"pref_id": "cs_001", "condition": "writing Python code", "action": "use snake_case for variables and functions, include type hints", "conflict_group": "naming_convention", "priority_context": ["python", "py"]}, {"pref_id": "cs_007", "condition": "I ask to improve or refactor code", "action": "address both logic and style, suggest modern idioms", "conflict_group": "review_scope", "priority_context": ["improve", "refactor", "better"]}, {"pref_id": "rf_004", "condition": "teaching a new concept", "action": "build up intuition before giving the formal definition", "conflict_group": "answer_position", "priority_context": ["learning", "explain", "why"]}, {"pref_id": "ds_006", "condition": "writing or reviewing documentation", "action": "be concise, avoid marketing language, focus on usage", "conflict_group": null, "priority_context": ["documentation", "docs", "readme"]}, {"pref_id": "ds_005", "condition": "discussing data structures", "action": "always include time complexity for operations mentioned", "conflict_group": null, "priority_context": ["data structure", "array", "tree", "hash"]}, {"pref_id": "ms_001", "condition": "solving algebraic equations", "action": "show each manipulation step with the operation applied noted", "conflict_group": "math_detail", "priority_context": ["solve", "equation", "algebra"]}, {"pref_id": "ec_003", "condition": "my code has a bug", "action": "show the bug location, explain why it's wrong, provide the fix", "conflict_group": null, "priority_context": ["bug", "error", "wrong"]}, {"pref_id": "cs_004", "condition": "the code snippet is short (under 20 lines)", "action": "include inline comments explaining each logical block", "conflict_group": "comment_style", "priority_context": ["example", "snippet"]}, {"pref_id": "ec_004", "condition": "I ask you to correct your previous response", "action": "acknowledge the error explicitly, then provide corrected version", "conflict_group": null, "priority_context": ["you were wrong", "that's not right", "actually"]}, {"pref_id": "cs_003", "condition": "writing SQL queries", "action": "use UPPERCASE for keywords, lowercase for table/column names", "conflict_group": "naming_convention", "priority_context": ["sql", "database", "query"]}, {"pref_id": "ip_001", "condition": "the task is complex with multiple parts", "action": "confirm the plan before executing, break into phases", "conflict_group": "autonomy", "priority_context": ["complex", "multiple", "project"]}, {"pref_id": "cs_005", "condition": "the code is a complete module or class", "action": "use docstrings at function/class level, minimal inline comments", "conflict_group": "comment_style", "priority_context": ["module", "class", "production"]}, {"pref_id": "ms_006", "condition": "I'm practicing for an exam", "action": "after solving, give a similar practice problem", "conflict_group": null, "priority_context": ["practice", "exam", "test"]}, {"pref_id": "ms_002", "condition": "discussing statistics or probability", "action": "start with intuition and real-world interpretation before formulas", "conflict_group": "math_approach", "priority_context": ["probability", "statistics", "likelihood"]}, {"pref_id": "cs_002", "condition": "writing JavaScript or TypeScript code", "action": "use camelCase for variables, PascalCase for classes", "conflict_group": "naming_convention", "priority_context": ["javascript", "js", "typescript", "ts"]}, {"pref_id": "ec_002", "condition": "I have a fundamental misconception", "action": "address the misconception directly and clearly before proceeding", "conflict_group": "correction_style", "priority_context": ["misconception", "fundamental_error"]}, {"pref_id": "ip_002", "condition": "I give a clear and specific instruction", "action": "execute directly without asking for confirmation", "conflict_group": "autonomy", "priority_context": ["do this", "make this", "specific_instruction"]}, {"pref_id": "ip_006", "condition": "I thank you or say the answer was helpful", "action": "don't add unnecessary follow-up, just acknowledge briefly", "conflict_group": null, "priority_context": ["thanks", "helpful", "great"]}, {"pref_id": "vb_003", "condition": "I ask 'why' or 'how come'", "action": "always explain the underlying reasoning, not just the what", "conflict_group": "explanation_depth", "priority_context": ["curiosity", "understanding"]}, {"pref_id": "cs_008", "condition": "providing error handling examples", "action": "always use specific exception types, never bare except", "conflict_group": null, "priority_context": ["error", "exception", "try"]}, {"pref_id": "rf_001", "condition": "listing multiple items or options", "action": "use bullet points with consistent indentation", "conflict_group": "format_structure", "priority_context": ["enumeration", "comparison"]}, {"pref_id": "vb_002", "condition": "the topic involves complex algorithms or mathematics", "action": "provide detailed step-by-step derivation with intermediate results", "conflict_group": "response_length", "priority_context": ["complex_topic", "proof", "derivation"]}, {"pref_id": "ip_005", "condition": "I express frustration or say 'this is annoying'", "action": "acknowledge the difficulty briefly, then provide direct help", "conflict_group": null, "priority_context": ["frustration", "annoying", "stuck"]}, {"pref_id": "ip_003", "condition": "I seem uncertain or ask 'what do you think'", "action": "provide a recommendation with brief rationale, not just options", "conflict_group": "guidance_style", "priority_context": ["uncertain", "should I", "what do you think"]}, {"pref_id": "ms_005", "condition": "discussing proofs", "action": "structure as: claim, approach sketch, formal proof, intuition recap", "conflict_group": "proof_style", "priority_context": ["prove", "proof", "show that"]}, {"pref_id": "oa_003", "condition": "any response with code", "action": "always specify the language in the code fence", "conflict_group": null, "priority_context": ["code"]}, {"pref_id": "oa_002", "condition": "teaching through code examples", "action": "break code into small chunks with explanation between each", "conflict_group": "code_presentation", "priority_context": ["teach", "learn", "understand"]}, {"pref_id": "ds_004", "condition": "explaining a theoretical concept", "action": "start with definition, then example, then edge cases", "conflict_group": "example_position", "priority_context": ["concept", "theory", "what is"]}], "conflict_groups": {"format_structure": ["rf_002", "rf_001"], "code_presentation": ["oa_001", "oa_002"], "correction_style": ["ec_001", "ec_002"], "math_detail": ["ms_004", "ms_001"], "review_scope": ["cs_006", "cs_007"], "response_length": ["vb_001", "vb_002"], "guidance_style": ["ip_004", "ip_003"], "explanation_depth": ["vb_004", "vb_003"], "answer_position": ["rf_003", "rf_004"], "naming_convention": ["cs_001", "cs_003", "cs_002"], "comment_style": ["cs_004", "cs_005"], "autonomy": ["ip_001", "ip_002"], "math_approach": ["ms_002"], "proof_style": ["ms_005"], "example_position": ["ds_004"]}, "meta": {"total_preferences": 42, "total_conflict_groups": 15, "generator": "schema_based"}} +{"user_id": "user_67ee9892", "persona": "A student preparing for technical interviews. Needs step-by-step problem solving practice.", "preferences": [{"pref_id": "ms_001", "condition": "solving algebraic equations", "action": "show each manipulation step with the operation applied noted", "conflict_group": "math_detail", "priority_context": ["solve", "equation", "algebra"]}, {"pref_id": "ms_003", "condition": "I ask to verify my calculation", "action": "check my work step by step, point out where I diverged if wrong", "conflict_group": null, "priority_context": ["verify", "check", "is this right"]}, {"pref_id": "ms_005", "condition": "discussing proofs", "action": "structure as: claim, approach sketch, formal proof, intuition recap", "conflict_group": "proof_style", "priority_context": ["prove", "proof", "show that"]}, {"pref_id": "ms_006", "condition": "I'm practicing for an exam", "action": "after solving, give a similar practice problem", "conflict_group": null, "priority_context": ["practice", "exam", "test"]}, {"pref_id": "rf_001", "condition": "listing multiple items or options", "action": "use bullet points with consistent indentation", "conflict_group": "format_structure", "priority_context": ["enumeration", "comparison"]}, {"pref_id": "cs_003", "condition": "writing SQL queries", "action": "use UPPERCASE for keywords, lowercase for table/column names", "conflict_group": "naming_convention", "priority_context": ["sql", "database", "query"]}, {"pref_id": "cs_001", "condition": "writing Python code", "action": "use snake_case for variables and functions, include type hints", "conflict_group": "naming_convention", "priority_context": ["python", "py"]}, {"pref_id": "ip_003", "condition": "I seem uncertain or ask 'what do you think'", "action": "provide a recommendation with brief rationale, not just options", "conflict_group": "guidance_style", "priority_context": ["uncertain", "should I", "what do you think"]}, {"pref_id": "vb_001", "condition": "I say 'quick question' or 'briefly'", "action": "respond in 3 sentences or fewer, no elaboration", "conflict_group": "response_length", "priority_context": ["time_pressure", "simple_query"]}, {"pref_id": "ec_002", "condition": "I have a fundamental misconception", "action": "address the misconception directly and clearly before proceeding", "conflict_group": "correction_style", "priority_context": ["misconception", "fundamental_error"]}, {"pref_id": "rf_002", "condition": "explaining a sequential process or procedure", "action": "use numbered steps with clear transitions", "conflict_group": "format_structure", "priority_context": ["tutorial", "how-to", "setup"]}, {"pref_id": "vb_002", "condition": "the topic involves complex algorithms or mathematics", "action": "provide detailed step-by-step derivation with intermediate results", "conflict_group": "response_length", "priority_context": ["complex_topic", "proof", "derivation"]}, {"pref_id": "ip_001", "condition": "the task is complex with multiple parts", "action": "confirm the plan before executing, break into phases", "conflict_group": "autonomy", "priority_context": ["complex", "multiple", "project"]}, {"pref_id": "cs_008", "condition": "providing error handling examples", "action": "always use specific exception types, never bare except", "conflict_group": null, "priority_context": ["error", "exception", "try"]}, {"pref_id": "vb_003", "condition": "I ask 'why' or 'how come'", "action": "always explain the underlying reasoning, not just the what", "conflict_group": "explanation_depth", "priority_context": ["curiosity", "understanding"]}, {"pref_id": "cs_004", "condition": "the code snippet is short (under 20 lines)", "action": "include inline comments explaining each logical block", "conflict_group": "comment_style", "priority_context": ["example", "snippet"]}, {"pref_id": "cs_005", "condition": "the code is a complete module or class", "action": "use docstrings at function/class level, minimal inline comments", "conflict_group": "comment_style", "priority_context": ["module", "class", "production"]}, {"pref_id": "cs_006", "condition": "I ask for a code review", "action": "focus only on bugs and logic errors, ignore style issues", "conflict_group": "review_scope", "priority_context": ["review", "check", "look at"]}, {"pref_id": "cs_002", "condition": "writing JavaScript or TypeScript code", "action": "use camelCase for variables, PascalCase for classes", "conflict_group": "naming_convention", "priority_context": ["javascript", "js", "typescript", "ts"]}, {"pref_id": "ds_003", "condition": "discussing APIs or library usage", "action": "show a minimal working example before explaining parameters", "conflict_group": "example_position", "priority_context": ["api", "library", "how to use"]}, {"pref_id": "ec_003", "condition": "my code has a bug", "action": "show the bug location, explain why it's wrong, provide the fix", "conflict_group": null, "priority_context": ["bug", "error", "wrong"]}, {"pref_id": "ds_005", "condition": "discussing data structures", "action": "always include time complexity for operations mentioned", "conflict_group": null, "priority_context": ["data structure", "array", "tree", "hash"]}, {"pref_id": "ip_002", "condition": "I give a clear and specific instruction", "action": "execute directly without asking for confirmation", "conflict_group": "autonomy", "priority_context": ["do this", "make this", "specific_instruction"]}, {"pref_id": "ms_002", "condition": "discussing statistics or probability", "action": "start with intuition and real-world interpretation before formulas", "conflict_group": "math_approach", "priority_context": ["probability", "statistics", "likelihood"]}, {"pref_id": "rf_003", "condition": "providing a final answer or conclusion", "action": "put the answer first, then explanation", "conflict_group": "answer_position", "priority_context": ["direct_question", "what_is"]}, {"pref_id": "ds_004", "condition": "explaining a theoretical concept", "action": "start with definition, then example, then edge cases", "conflict_group": "example_position", "priority_context": ["concept", "theory", "what is"]}, {"pref_id": "ds_006", "condition": "writing or reviewing documentation", "action": "be concise, avoid marketing language, focus on usage", "conflict_group": null, "priority_context": ["documentation", "docs", "readme"]}, {"pref_id": "ds_002", "condition": "discussing system design or architecture", "action": "describe components as a list first, then explain interactions", "conflict_group": null, "priority_context": ["design", "architecture", "system"]}, {"pref_id": "ip_005", "condition": "I express frustration or say 'this is annoying'", "action": "acknowledge the difficulty briefly, then provide direct help", "conflict_group": null, "priority_context": ["frustration", "annoying", "stuck"]}, {"pref_id": "oa_002", "condition": "teaching through code examples", "action": "break code into small chunks with explanation between each", "conflict_group": "code_presentation", "priority_context": ["teach", "learn", "understand"]}, {"pref_id": "ec_004", "condition": "I ask you to correct your previous response", "action": "acknowledge the error explicitly, then provide corrected version", "conflict_group": null, "priority_context": ["you were wrong", "that's not right", "actually"]}, {"pref_id": "ms_004", "condition": "the problem involves calculus", "action": "state the rule being applied (chain rule, integration by parts, etc.)", "conflict_group": "math_detail", "priority_context": ["derivative", "integral", "calculus"]}, {"pref_id": "oa_004", "condition": "providing commands to run", "action": "use bash code blocks, include expected output as comments", "conflict_group": null, "priority_context": ["command", "run", "terminal"]}, {"pref_id": "ds_001", "condition": "discussing machine learning concepts", "action": "include the mathematical formulation alongside intuitive explanation", "conflict_group": null, "priority_context": ["ml", "machine learning", "model"]}, {"pref_id": "vb_004", "condition": "I'm debugging and say 'it doesn't work'", "action": "focus on diagnosing the specific issue, skip general explanations", "conflict_group": "explanation_depth", "priority_context": ["debugging", "error", "fix"]}, {"pref_id": "ip_006", "condition": "I thank you or say the answer was helpful", "action": "don't add unnecessary follow-up, just acknowledge briefly", "conflict_group": null, "priority_context": ["thanks", "helpful", "great"]}, {"pref_id": "rf_004", "condition": "teaching a new concept", "action": "build up intuition before giving the formal definition", "conflict_group": "answer_position", "priority_context": ["learning", "explain", "why"]}], "conflict_groups": {"math_detail": ["ms_001", "ms_004"], "proof_style": ["ms_005"], "format_structure": ["rf_001", "rf_002"], "naming_convention": ["cs_003", "cs_001", "cs_002"], "guidance_style": ["ip_003"], "response_length": ["vb_001", "vb_002"], "correction_style": ["ec_002"], "autonomy": ["ip_001", "ip_002"], "explanation_depth": ["vb_003", "vb_004"], "comment_style": ["cs_004", "cs_005"], "review_scope": ["cs_006"], "example_position": ["ds_003", "ds_004"], "math_approach": ["ms_002"], "answer_position": ["rf_003", "rf_004"], "code_presentation": ["oa_002"]}, "meta": {"total_preferences": 37, "total_conflict_groups": 15, "generator": "schema_based"}} +{"user_id": "user_78da9d75", "persona": "A junior developer learning full-stack. Prefers patient, incremental explanations with examples.", "preferences": [{"pref_id": "ip_003", "condition": "I seem uncertain or ask 'what do you think'", "action": "provide a recommendation with brief rationale, not just options", "conflict_group": "guidance_style", "priority_context": ["uncertain", "should I", "what do you think"]}, {"pref_id": "ip_006", "condition": "I thank you or say the answer was helpful", "action": "don't add unnecessary follow-up, just acknowledge briefly", "conflict_group": null, "priority_context": ["thanks", "helpful", "great"]}, {"pref_id": "ip_002", "condition": "I give a clear and specific instruction", "action": "execute directly without asking for confirmation", "conflict_group": "autonomy", "priority_context": ["do this", "make this", "specific_instruction"]}, {"pref_id": "ms_005", "condition": "discussing proofs", "action": "structure as: claim, approach sketch, formal proof, intuition recap", "conflict_group": "proof_style", "priority_context": ["prove", "proof", "show that"]}, {"pref_id": "cs_003", "condition": "writing SQL queries", "action": "use UPPERCASE for keywords, lowercase for table/column names", "conflict_group": "naming_convention", "priority_context": ["sql", "database", "query"]}, {"pref_id": "ds_003", "condition": "discussing APIs or library usage", "action": "show a minimal working example before explaining parameters", "conflict_group": "example_position", "priority_context": ["api", "library", "how to use"]}, {"pref_id": "ec_003", "condition": "my code has a bug", "action": "show the bug location, explain why it's wrong, provide the fix", "conflict_group": null, "priority_context": ["bug", "error", "wrong"]}, {"pref_id": "ip_001", "condition": "the task is complex with multiple parts", "action": "confirm the plan before executing, break into phases", "conflict_group": "autonomy", "priority_context": ["complex", "multiple", "project"]}, {"pref_id": "ds_005", "condition": "discussing data structures", "action": "always include time complexity for operations mentioned", "conflict_group": null, "priority_context": ["data structure", "array", "tree", "hash"]}, {"pref_id": "ms_001", "condition": "solving algebraic equations", "action": "show each manipulation step with the operation applied noted", "conflict_group": "math_detail", "priority_context": ["solve", "equation", "algebra"]}, {"pref_id": "vb_002", "condition": "the topic involves complex algorithms or mathematics", "action": "provide detailed step-by-step derivation with intermediate results", "conflict_group": "response_length", "priority_context": ["complex_topic", "proof", "derivation"]}, {"pref_id": "ip_005", "condition": "I express frustration or say 'this is annoying'", "action": "acknowledge the difficulty briefly, then provide direct help", "conflict_group": null, "priority_context": ["frustration", "annoying", "stuck"]}, {"pref_id": "rf_003", "condition": "providing a final answer or conclusion", "action": "put the answer first, then explanation", "conflict_group": "answer_position", "priority_context": ["direct_question", "what_is"]}, {"pref_id": "cs_005", "condition": "the code is a complete module or class", "action": "use docstrings at function/class level, minimal inline comments", "conflict_group": "comment_style", "priority_context": ["module", "class", "production"]}, {"pref_id": "ms_003", "condition": "I ask to verify my calculation", "action": "check my work step by step, point out where I diverged if wrong", "conflict_group": null, "priority_context": ["verify", "check", "is this right"]}, {"pref_id": "ip_004", "condition": "I'm comparing alternatives", "action": "present trade-offs in a table format with clear criteria", "conflict_group": "guidance_style", "priority_context": ["compare", "vs", "or", "which"]}, {"pref_id": "ds_004", "condition": "explaining a theoretical concept", "action": "start with definition, then example, then edge cases", "conflict_group": "example_position", "priority_context": ["concept", "theory", "what is"]}, {"pref_id": "ds_006", "condition": "writing or reviewing documentation", "action": "be concise, avoid marketing language, focus on usage", "conflict_group": null, "priority_context": ["documentation", "docs", "readme"]}, {"pref_id": "cs_001", "condition": "writing Python code", "action": "use snake_case for variables and functions, include type hints", "conflict_group": "naming_convention", "priority_context": ["python", "py"]}, {"pref_id": "cs_002", "condition": "writing JavaScript or TypeScript code", "action": "use camelCase for variables, PascalCase for classes", "conflict_group": "naming_convention", "priority_context": ["javascript", "js", "typescript", "ts"]}, {"pref_id": "rf_004", "condition": "teaching a new concept", "action": "build up intuition before giving the formal definition", "conflict_group": "answer_position", "priority_context": ["learning", "explain", "why"]}, {"pref_id": "ds_002", "condition": "discussing system design or architecture", "action": "describe components as a list first, then explain interactions", "conflict_group": null, "priority_context": ["design", "architecture", "system"]}, {"pref_id": "ec_001", "condition": "I make a minor error in terminology", "action": "correct it gently inline without making it a focus", "conflict_group": "correction_style", "priority_context": ["minor_error", "terminology"]}, {"pref_id": "oa_002", "condition": "teaching through code examples", "action": "break code into small chunks with explanation between each", "conflict_group": "code_presentation", "priority_context": ["teach", "learn", "understand"]}, {"pref_id": "oa_003", "condition": "any response with code", "action": "always specify the language in the code fence", "conflict_group": null, "priority_context": ["code"]}, {"pref_id": "cs_006", "condition": "I ask for a code review", "action": "focus only on bugs and logic errors, ignore style issues", "conflict_group": "review_scope", "priority_context": ["review", "check", "look at"]}, {"pref_id": "ds_001", "condition": "discussing machine learning concepts", "action": "include the mathematical formulation alongside intuitive explanation", "conflict_group": null, "priority_context": ["ml", "machine learning", "model"]}, {"pref_id": "ec_002", "condition": "I have a fundamental misconception", "action": "address the misconception directly and clearly before proceeding", "conflict_group": "correction_style", "priority_context": ["misconception", "fundamental_error"]}, {"pref_id": "ec_004", "condition": "I ask you to correct your previous response", "action": "acknowledge the error explicitly, then provide corrected version", "conflict_group": null, "priority_context": ["you were wrong", "that's not right", "actually"]}, {"pref_id": "vb_004", "condition": "I'm debugging and say 'it doesn't work'", "action": "focus on diagnosing the specific issue, skip general explanations", "conflict_group": "explanation_depth", "priority_context": ["debugging", "error", "fix"]}, {"pref_id": "cs_007", "condition": "I ask to improve or refactor code", "action": "address both logic and style, suggest modern idioms", "conflict_group": "review_scope", "priority_context": ["improve", "refactor", "better"]}, {"pref_id": "cs_008", "condition": "providing error handling examples", "action": "always use specific exception types, never bare except", "conflict_group": null, "priority_context": ["error", "exception", "try"]}, {"pref_id": "rf_001", "condition": "listing multiple items or options", "action": "use bullet points with consistent indentation", "conflict_group": "format_structure", "priority_context": ["enumeration", "comparison"]}, {"pref_id": "cs_004", "condition": "the code snippet is short (under 20 lines)", "action": "include inline comments explaining each logical block", "conflict_group": "comment_style", "priority_context": ["example", "snippet"]}, {"pref_id": "vb_003", "condition": "I ask 'why' or 'how come'", "action": "always explain the underlying reasoning, not just the what", "conflict_group": "explanation_depth", "priority_context": ["curiosity", "understanding"]}, {"pref_id": "vb_001", "condition": "I say 'quick question' or 'briefly'", "action": "respond in 3 sentences or fewer, no elaboration", "conflict_group": "response_length", "priority_context": ["time_pressure", "simple_query"]}, {"pref_id": "ms_002", "condition": "discussing statistics or probability", "action": "start with intuition and real-world interpretation before formulas", "conflict_group": "math_approach", "priority_context": ["probability", "statistics", "likelihood"]}, {"pref_id": "ms_004", "condition": "the problem involves calculus", "action": "state the rule being applied (chain rule, integration by parts, etc.)", "conflict_group": "math_detail", "priority_context": ["derivative", "integral", "calculus"]}, {"pref_id": "vb_005", "condition": "I explicitly share my current understanding first", "action": "acknowledge what I got right, then correct only the gaps", "conflict_group": null, "priority_context": ["validation", "checking"]}, {"pref_id": "ms_006", "condition": "I'm practicing for an exam", "action": "after solving, give a similar practice problem", "conflict_group": null, "priority_context": ["practice", "exam", "test"]}, {"pref_id": "rf_002", "condition": "explaining a sequential process or procedure", "action": "use numbered steps with clear transitions", "conflict_group": "format_structure", "priority_context": ["tutorial", "how-to", "setup"]}], "conflict_groups": {"guidance_style": ["ip_003", "ip_004"], "autonomy": ["ip_002", "ip_001"], "proof_style": ["ms_005"], "naming_convention": ["cs_003", "cs_001", "cs_002"], "example_position": ["ds_003", "ds_004"], "math_detail": ["ms_001", "ms_004"], "response_length": ["vb_002", "vb_001"], "answer_position": ["rf_003", "rf_004"], "comment_style": ["cs_005", "cs_004"], "correction_style": ["ec_001", "ec_002"], "code_presentation": ["oa_002"], "review_scope": ["cs_006", "cs_007"], "explanation_depth": ["vb_004", "vb_003"], "format_structure": ["rf_001", "rf_002"], "math_approach": ["ms_002"]}, "meta": {"total_preferences": 41, "total_conflict_groups": 15, "generator": "schema_based"}} diff --git a/collaborativeagents/scripts/analyze_vector_preference.py b/collaborativeagents/scripts/analyze_vector_preference.py new file mode 100755 index 0000000..7079b26 --- /dev/null +++ b/collaborativeagents/scripts/analyze_vector_preference.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +""" +分析 user vector 与 revealed preference 之间的关联强度 +""" +import json +import numpy as np +from pathlib import Path +import sys + +def load_experiment(exp_dir): + """加载实验结果""" + exp_path = Path(exp_dir) + + # 找到结果目录 + for method in ["rag_vector", "rag_vector_fast", "rag_vector_balanced"]: + for sub in exp_path.iterdir(): + result_dir = sub / method + if result_dir.exists(): + vectors_path = result_dir / "user_vectors.npz" + results_path = result_dir / "results.json" + if vectors_path.exists() and results_path.exists(): + return { + "vectors": np.load(vectors_path, allow_pickle=True), + "results": json.load(open(results_path)), + "method": method + } + return None + +def analyze_vectors(data): + """分析user vectors""" + vectors = data["vectors"] + results = data["results"] + + user_ids = vectors["user_ids"] + z_long = vectors["z_long"] + z_short = vectors["z_short"] + + print(f"=== User Vector 分析 ===") + print(f"用户数: {len(user_ids)}") + print(f"Vector维度: {z_long.shape[1]}") + + # 计算非零vector数量 + z_long_norms = np.linalg.norm(z_long, axis=1) + z_short_norms = np.linalg.norm(z_short, axis=1) + + nonzero_long = np.count_nonzero(z_long_norms) + nonzero_short = np.count_nonzero(z_short_norms) + + print(f"\nz_long 非零用户: {nonzero_long}/{len(user_ids)}") + print(f"z_short 非零用户: {nonzero_short}/{len(user_ids)}") + print(f"z_long norm 均值: {np.mean(z_long_norms):.4f}") + print(f"z_short norm 均值: {np.mean(z_short_norms):.4f}") + + # 按用户分析性能与vector norm的关系 + print(f"\n=== Vector Norm vs 性能 ===") + + user_stats = {} + for s in results: + uid = s.get("profile_id", "") + if uid not in user_stats: + user_stats[uid] = {"success": 0, "total": 0, "enforce": 0} + m = s.get("metrics", {}) + user_stats[uid]["total"] += 1 + user_stats[uid]["success"] += 1 if m.get("task_success", False) else 0 + user_stats[uid]["enforce"] += m.get("enforcement_count", 0) + + # 计算相关性 + success_rates = [] + norms = [] + + for i, uid in enumerate(user_ids): + if uid in user_stats and user_stats[uid]["total"] > 0: + sr = user_stats[uid]["success"] / user_stats[uid]["total"] + success_rates.append(sr) + norms.append(z_long_norms[i]) + + if len(success_rates) > 5: + corr = np.corrcoef(success_rates, norms)[0, 1] + print(f"z_long norm vs 成功率 相关系数: {corr:.4f}") + + return { + "n_users": len(user_ids), + "nonzero_long": nonzero_long, + "nonzero_short": nonzero_short, + "mean_norm_long": float(np.mean(z_long_norms)), + "mean_norm_short": float(np.mean(z_short_norms)), + } + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Usage: python analyze_vector_preference.py <experiment_dir>") + print("Example: python analyze_vector_preference.py collaborativeagents/results/rag_vector_v3") + sys.exit(1) + + exp_dir = sys.argv[1] + data = load_experiment(exp_dir) + + if data is None: + print(f"未找到有效的rag_vector实验结果: {exp_dir}") + sys.exit(1) + + print(f"加载实验: {data['method']}") + analyze_vectors(data) diff --git a/collaborativeagents/scripts/conflict_scenario_generator.py b/collaborativeagents/scripts/conflict_scenario_generator.py index 9d00de8..eaf8ef2 100644 --- a/collaborativeagents/scripts/conflict_scenario_generator.py +++ b/collaborativeagents/scripts/conflict_scenario_generator.py @@ -367,11 +367,14 @@ class ConflictScenarioGenerator: # Find conflict groups in these preferences conflict_groups = {} for pref in preferences: - cg = pref.get('conflict_group') - if cg: - if cg not in conflict_groups: - conflict_groups[cg] = [] - conflict_groups[cg].append(pref) + # Handle both dict preferences (with conflict_group) and string preferences + if isinstance(pref, dict): + cg = pref.get('conflict_group') + if cg: + if cg not in conflict_groups: + conflict_groups[cg] = [] + conflict_groups[cg].append(pref) + # String preferences don't have conflict groups - skip them # Find a conflict group with at least 2 preferences for cg, prefs in conflict_groups.items(): diff --git a/collaborativeagents/scripts/queue_next_run.sh b/collaborativeagents/scripts/queue_next_run.sh new file mode 100755 index 0000000..524f0e2 --- /dev/null +++ b/collaborativeagents/scripts/queue_next_run.sh @@ -0,0 +1,44 @@ +#!/bin/bash +# Wait for current fullrun_4methods to finish, then start real-profile experiment + +LOG="/workspace/personalization-user-model/collaborativeagents/results/fullrun_4methods.log" +SCRIPTS_DIR="/workspace/personalization-user-model/collaborativeagents/scripts" + +echo "[$(date)] Waiting for fullrun_4methods to complete..." + +while true; do + if grep -q "EXPERIMENT COMPLETE" "$LOG" 2>/dev/null; then + echo "[$(date)] fullrun_4methods completed!" + break + fi + # Check if process died without completing + if ! pgrep -f "fullrun_4methods" > /dev/null 2>&1; then + if ! grep -q "EXPERIMENT COMPLETE" "$LOG" 2>/dev/null; then + echo "[$(date)] WARNING: process died before completion. Starting next run anyway." + break + fi + fi + sleep 60 +done + +echo "[$(date)] Starting real-profile experiment: 4 methods x 100 profiles x 20 sessions" + +cd "$SCRIPTS_DIR" +nohup python3 run_experiments.py \ + --methods vanilla,reflection,rag,rag_vector \ + --datasets math-hard \ + --n-profiles 100 \ + --n-sessions 20 \ + --max-turns 8 \ + --use-vllm \ + --vllm-agent-url http://localhost:8003/v1 \ + --vllm-user-url http://localhost:8004/v1 \ + --parallel-profiles 100 \ + --reward-mode llm_local \ + --reward-vllm-url http://localhost:8005/v1 \ + --profile-path ../data/complex_profiles_v2/profiles_200.jsonl \ + --output-dir ../results/realprofile_4methods \ + > ../results/realprofile_4methods.log 2>&1 & + +echo "[$(date)] Experiment launched with PID $!" +echo "Log: /workspace/personalization-user-model/collaborativeagents/results/realprofile_4methods.log" diff --git a/collaborativeagents/scripts/queue_rag_60s.sh b/collaborativeagents/scripts/queue_rag_60s.sh new file mode 100755 index 0000000..87d6679 --- /dev/null +++ b/collaborativeagents/scripts/queue_rag_60s.sh @@ -0,0 +1,45 @@ +#!/bin/bash +# 等待 reflection_60s 完成后启动 rag 60 session 实验 + +echo "等待 reflection_60s 完成..." + +while true; do + CKPT=$(ls collaborativeagents/results/reflection_60s/*/reflection/checkpoint.json 2>/dev/null | head -1) + if [ -n "$CKPT" ]; then + PROGRESS=$(cat "$CKPT" | python3 -c " +import json, sys +data = json.load(sys.stdin) +total = sum(data['sessions_per_profile'].values()) +print(total) +" 2>/dev/null) + + if [ "$PROGRESS" = "3600" ]; then + echo "$(date '+%H:%M:%S') reflection_60s 已完成" + break + fi + echo "$(date '+%H:%M:%S') reflection_60s 进度: $PROGRESS/3600" + else + echo "$(date '+%H:%M:%S') 等待reflection_60s启动..." + fi + sleep 60 +done + +echo "启动 rag_60s..." + +nohup python collaborativeagents/scripts/run_experiments.py \ + --methods rag \ + --datasets math-hard,math-500,bigcodebench \ + --n-profiles 60 \ + --n-sessions 60 \ + --max-turns 10 \ + --use-vllm \ + --vllm-agent-url http://localhost:8003/v1 \ + --vllm-user-url http://localhost:8004/v1 \ + --use-batch-processing \ + --batch-size 4 \ + --parallel-profiles 20 \ + --profile-path collaborativeagents/data/complex_profiles_v2/profiles_200.jsonl \ + --output-dir collaborativeagents/results/rag_60s \ + > collaborativeagents/results/rag_60s.log 2>&1 & + +echo "rag_60s 已启动,PID: $!" diff --git a/collaborativeagents/scripts/queue_rag_rewrite.sh b/collaborativeagents/scripts/queue_rag_rewrite.sh new file mode 100755 index 0000000..a5b5b99 --- /dev/null +++ b/collaborativeagents/scripts/queue_rag_rewrite.sh @@ -0,0 +1,45 @@ +#!/bin/bash +# 等待 rag_vector_60s 完成后启动 rag_rewrite 测试 + +echo "等待 rag_vector_60s 完成..." + +while true; do + CKPT=$(ls collaborativeagents/results/rag_vector_60s/*/rag_vector/checkpoint.json 2>/dev/null | head -1) + if [ -n "$CKPT" ]; then + PROGRESS=$(cat "$CKPT" | python3 -c " +import json, sys +data = json.load(sys.stdin) +total = sum(data['sessions_per_profile'].values()) +print(total) +" 2>/dev/null) + + if [ "$PROGRESS" = "3600" ]; then + echo "$(date '+%H:%M:%S') rag_vector_60s 已完成" + break + fi + echo "$(date '+%H:%M:%S') rag_vector_60s 进度: $PROGRESS/3600" + else + echo "$(date '+%H:%M:%S') 等待rag_vector_60s启动..." + fi + sleep 60 +done + +echo "启动 rag_rewrite_60s..." + +nohup python collaborativeagents/scripts/run_experiments.py \ + --methods rag_rewrite \ + --datasets math-hard,math-500,bigcodebench \ + --n-profiles 60 \ + --n-sessions 60 \ + --max-turns 10 \ + --use-vllm \ + --vllm-agent-url http://localhost:8003/v1 \ + --vllm-user-url http://localhost:8004/v1 \ + --use-batch-processing \ + --batch-size 4 \ + --parallel-profiles 20 \ + --profile-path collaborativeagents/data/complex_profiles_v2/profiles_200.jsonl \ + --output-dir collaborativeagents/results/rag_rewrite_60s \ + > collaborativeagents/results/rag_rewrite_60s.log 2>&1 & + +echo "rag_rewrite_60s 已启动,PID: $!" diff --git a/collaborativeagents/scripts/queue_rag_rewrite_vector.sh b/collaborativeagents/scripts/queue_rag_rewrite_vector.sh new file mode 100755 index 0000000..480e3ac --- /dev/null +++ b/collaborativeagents/scripts/queue_rag_rewrite_vector.sh @@ -0,0 +1,45 @@ +#!/bin/bash +# 等待 rag_rewrite_60s 完成后启动 rag_rewrite_vector 测试 + +echo "等待 rag_rewrite_60s 完成..." + +while true; do + CKPT=$(ls collaborativeagents/results/rag_rewrite_60s/*/rag_rewrite/checkpoint.json 2>/dev/null | head -1) + if [ -n "$CKPT" ]; then + PROGRESS=$(cat "$CKPT" | python3 -c " +import json, sys +data = json.load(sys.stdin) +total = sum(data['sessions_per_profile'].values()) +print(total) +" 2>/dev/null) + + if [ "$PROGRESS" = "3600" ]; then + echo "$(date '+%H:%M:%S') rag_rewrite_60s 已完成" + break + fi + echo "$(date '+%H:%M:%S') rag_rewrite_60s 进度: $PROGRESS/3600" + else + echo "$(date '+%H:%M:%S') 等待rag_rewrite_60s启动..." + fi + sleep 120 +done + +echo "启动 rag_rewrite_vector_60s..." + +nohup python collaborativeagents/scripts/run_experiments.py \ + --methods rag_rewrite_vector \ + --datasets math-hard,math-500,bigcodebench \ + --n-profiles 60 \ + --n-sessions 60 \ + --max-turns 10 \ + --use-vllm \ + --vllm-agent-url http://localhost:8003/v1 \ + --vllm-user-url http://localhost:8004/v1 \ + --use-batch-processing \ + --batch-size 4 \ + --parallel-profiles 20 \ + --profile-path collaborativeagents/data/complex_profiles_v2/profiles_200.jsonl \ + --output-dir collaborativeagents/results/rag_rewrite_vector_60s \ + > collaborativeagents/results/rag_rewrite_vector_60s.log 2>&1 & + +echo "rag_rewrite_vector_60s 已启动,PID: $!" diff --git a/collaborativeagents/scripts/queue_rag_vector.sh b/collaborativeagents/scripts/queue_rag_vector.sh new file mode 100755 index 0000000..2760dc7 --- /dev/null +++ b/collaborativeagents/scripts/queue_rag_vector.sh @@ -0,0 +1,45 @@ +#!/bin/bash +# 等待 topk5_v3 完成后启动 rag_vector 实验 + +echo "等待 topk5_v3 完成..." + +while true; do + CKPT=$(ls collaborativeagents/results/rag_topk5_v3/*/rag/checkpoint.json 2>/dev/null | head -1) + if [ -n "$CKPT" ]; then + PROGRESS=$(cat "$CKPT" | python3 -c " +import json, sys +data = json.load(sys.stdin) +total = sum(data['sessions_per_profile'].values()) +print(total) +" 2>/dev/null) + + if [ "$PROGRESS" = "1800" ]; then + echo "$(date '+%H:%M:%S') topk5_v3 已完成" + break + fi + echo "$(date '+%H:%M:%S') topk5_v3 进度: $PROGRESS/1800" + else + echo "$(date '+%H:%M:%S') 等待topk5_v3启动..." + fi + sleep 60 +done + +echo "启动 rag_vector..." + +nohup python collaborativeagents/scripts/run_experiments.py \ + --methods rag_vector \ + --datasets math-hard,math-500,bigcodebench \ + --n-profiles 60 \ + --n-sessions 30 \ + --max-turns 10 \ + --use-vllm \ + --vllm-agent-url http://localhost:8003/v1 \ + --vllm-user-url http://localhost:8004/v1 \ + --use-batch-processing \ + --batch-size 4 \ + --parallel-profiles 20 \ + --profile-path collaborativeagents/data/complex_profiles_v2/profiles_200.jsonl \ + --output-dir collaborativeagents/results/rag_vector_v3 \ + > collaborativeagents/results/rag_vector_v3.log 2>&1 & + +echo "rag_vector_v3 已启动,PID: $!" diff --git a/collaborativeagents/scripts/queue_reflection_60s.sh b/collaborativeagents/scripts/queue_reflection_60s.sh new file mode 100755 index 0000000..6d15073 --- /dev/null +++ b/collaborativeagents/scripts/queue_reflection_60s.sh @@ -0,0 +1,45 @@ +#!/bin/bash +# 等待 rag_vector_v3 完成后启动 reflection 60 session 实验 + +echo "等待 rag_vector_v3 完成..." + +while true; do + CKPT=$(ls collaborativeagents/results/rag_vector_v3/*/rag_vector/checkpoint.json 2>/dev/null | head -1) + if [ -n "$CKPT" ]; then + PROGRESS=$(cat "$CKPT" | python3 -c " +import json, sys +data = json.load(sys.stdin) +total = sum(data['sessions_per_profile'].values()) +print(total) +" 2>/dev/null) + + if [ "$PROGRESS" = "1800" ]; then + echo "$(date '+%H:%M:%S') rag_vector_v3 已完成" + break + fi + echo "$(date '+%H:%M:%S') rag_vector_v3 进度: $PROGRESS/1800" + else + echo "$(date '+%H:%M:%S') 等待rag_vector_v3启动..." + fi + sleep 60 +done + +echo "启动 reflection_60s..." + +nohup python collaborativeagents/scripts/run_experiments.py \ + --methods reflection \ + --datasets math-hard,math-500,bigcodebench \ + --n-profiles 60 \ + --n-sessions 60 \ + --max-turns 10 \ + --use-vllm \ + --vllm-agent-url http://localhost:8003/v1 \ + --vllm-user-url http://localhost:8004/v1 \ + --use-batch-processing \ + --batch-size 4 \ + --parallel-profiles 20 \ + --profile-path collaborativeagents/data/complex_profiles_v2/profiles_200.jsonl \ + --output-dir collaborativeagents/results/reflection_60s \ + > collaborativeagents/results/reflection_60s.log 2>&1 & + +echo "reflection_60s 已启动,PID: $!" diff --git a/collaborativeagents/scripts/queue_topk5_v2.sh b/collaborativeagents/scripts/queue_topk5_v2.sh new file mode 100755 index 0000000..a116a28 --- /dev/null +++ b/collaborativeagents/scripts/queue_topk5_v2.sh @@ -0,0 +1,44 @@ +#!/bin/bash +# 等待 rag_dynamic 实验完成后启动 topk5 新版本测试 + +echo "等待 rag_dynamic 实验完成..." + +while true; do + # 检查是否有运行中的实验进程 + if pgrep -f "rag_dynamic" > /dev/null; then + # 检查checkpoint进度 + PROGRESS=$(cat collaborativeagents/results/rag_dynamic_test/*/rag_dynamic/checkpoint.json 2>/dev/null | python3 -c " +import json, sys +try: + data = json.load(sys.stdin) + total = sum(data['sessions_per_profile'].values()) + print(f'{total}/1800') +except: + print('0/1800') +" 2>/dev/null) + echo "$(date '+%H:%M:%S') rag_dynamic 进度: $PROGRESS" + sleep 60 + else + echo "rag_dynamic 已完成或未运行,启动 topk5_v2..." + break + fi +done + +# 启动新实验 +nohup python collaborativeagents/scripts/run_experiments.py \ + --methods rag \ + --datasets math-hard,math-500,bigcodebench \ + --n-profiles 60 \ + --n-sessions 30 \ + --max-turns 10 \ + --use-vllm \ + --vllm-agent-url http://localhost:8003/v1 \ + --vllm-user-url http://localhost:8004/v1 \ + --use-batch-processing \ + --batch-size 4 \ + --parallel-profiles 20 \ + --profile-path collaborativeagents/data/complex_profiles_v2/profiles_200.jsonl \ + --output-dir collaborativeagents/results/rag_topk5_v2 \ + > collaborativeagents/results/rag_topk5_v2.log 2>&1 & + +echo "topk5_v2 已启动,PID: $!" diff --git a/collaborativeagents/scripts/queue_topk5_v3.sh b/collaborativeagents/scripts/queue_topk5_v3.sh new file mode 100755 index 0000000..e93b066 --- /dev/null +++ b/collaborativeagents/scripts/queue_topk5_v3.sh @@ -0,0 +1,52 @@ +#!/bin/bash +# 等待 reflection_v2 实验完成后启动 topk5_v3 测试 + +echo "等待 reflection_v2 实验完成..." + +while true; do + # 检查checkpoint进度 + CKPT=$(ls collaborativeagents/results/reflection_v2/*/reflection/checkpoint.json 2>/dev/null | head -1) + if [ -n "$CKPT" ]; then + PROGRESS=$(cat "$CKPT" | python3 -c " +import json, sys +try: + data = json.load(sys.stdin) + total = sum(data['sessions_per_profile'].values()) + print(f'{total}/1800 ({total*100//1800}%)') +except: + print('0/1800') +" 2>/dev/null) + + # 检查是否完成 + DONE=$(echo "$PROGRESS" | grep -c "1800/1800") + if [ "$DONE" -eq 1 ]; then + echo "$(date '+%H:%M:%S') reflection_v2 已完成" + break + fi + echo "$(date '+%H:%M:%S') reflection_v2 进度: $PROGRESS" + else + echo "$(date '+%H:%M:%S') 等待reflection_v2启动..." + fi + sleep 60 +done + +echo "启动 topk5_v3..." + +# 启动新实验 +nohup python collaborativeagents/scripts/run_experiments.py \ + --methods rag \ + --datasets math-hard,math-500,bigcodebench \ + --n-profiles 60 \ + --n-sessions 30 \ + --max-turns 10 \ + --use-vllm \ + --vllm-agent-url http://localhost:8003/v1 \ + --vllm-user-url http://localhost:8004/v1 \ + --use-batch-processing \ + --batch-size 4 \ + --parallel-profiles 20 \ + --profile-path collaborativeagents/data/complex_profiles_v2/profiles_200.jsonl \ + --output-dir collaborativeagents/results/rag_topk5_v3 \ + > collaborativeagents/results/rag_topk5_v3.log 2>&1 & + +echo "topk5_v3 已启动,PID: $!" diff --git a/collaborativeagents/scripts/run_experiments.py b/collaborativeagents/scripts/run_experiments.py index e04680c..da3549b 100644 --- a/collaborativeagents/scripts/run_experiments.py +++ b/collaborativeagents/scripts/run_experiments.py @@ -15,6 +15,7 @@ import json import yaml import os import sys +import numpy as np from pathlib import Path from datetime import datetime from typing import List, Dict, Any, Optional @@ -113,7 +114,13 @@ AVAILABLE_METHODS = { "reflection_grpo": "Reflection + GRPO training", "all_memory": "All extracted memories in context (no retrieval)", "rag": "Extractor + RAG (no user vector)", + "rag_dynamic": "Extractor + RAG with dynamic topk (min=3, max=8, ratio=0.5)", + "rag_rewrite": "Extractor + RAG with LLM preference rewrite/merge", + "rag_rewrite_vector": "Extractor + RAG + user vector + LLM preference rewrite", "rag_vector": "Extractor + RAG + user vector (proposed method)", + "rag_vector_fast": "Extractor + RAG + user vector with 10x learning rate", + "rag_vector_consolidate": "Extractor + RAG + user vector with session-level preference consolidation", + "rag_vector_balanced": "Extractor + RAG + user vector with balanced rewards (10x LR + positive signal for good turns)", "rag_bge": "Extractor + RAG with BGE reranker (278M)", "rag_vector_bge": "Extractor + RAG + user vector with BGE reranker (278M)", } @@ -256,6 +263,68 @@ class ExperimentRunner: # Profile will be passed to start_session() when the conversation begins return adapter + def _export_user_vectors(self, method: str, adapters: Dict[int, Any]) -> None: + """ + Export user vectors from all adapters to disk for later analysis. + + Saves both .npz (efficient numpy format) and .json (human-readable). + + Args: + method: Method name for the output directory + adapters: Dict mapping profile_idx to adapter instances + """ + method_dir = self.output_dir / method + + # Collect all user vectors from adapters + all_vectors = {} + for profile_idx, adapter in adapters.items(): + if hasattr(adapter, 'export_all_user_vectors'): + vectors = adapter.export_all_user_vectors() + all_vectors.update(vectors) + + if not all_vectors: + logger.info(f" No user vectors to export for {method}") + return + + # Save as .npz for efficient analysis + npz_path = method_dir / "user_vectors.npz" + user_ids = list(all_vectors.keys()) + k = len(all_vectors[user_ids[0]]["z_long"]) + z_long = np.zeros((len(user_ids), k), dtype=np.float32) + z_short = np.zeros((len(user_ids), k), dtype=np.float32) + reward_ma = np.zeros(len(user_ids), dtype=np.float32) + + for i, uid in enumerate(user_ids): + z_long[i] = all_vectors[uid]["z_long"] + z_short[i] = all_vectors[uid]["z_short"] + reward_ma[i] = all_vectors[uid]["reward_ma"] + + np.savez( + npz_path, + user_ids=np.array(user_ids), + z_long=z_long, + z_short=z_short, + reward_ma=reward_ma, + ) + + # Also save summary stats as JSON + summary = { + "n_users": len(user_ids), + "vector_dim": k, + "z_long_norms": {uid: all_vectors[uid]["z_long_norm"] for uid in user_ids}, + "z_short_norms": {uid: all_vectors[uid]["z_short_norm"] for uid in user_ids}, + "reward_mas": {uid: all_vectors[uid]["reward_ma"] for uid in user_ids}, + "stats": { + "z_long_norm_mean": float(np.mean([all_vectors[uid]["z_long_norm"] for uid in user_ids])), + "z_long_norm_max": float(np.max([all_vectors[uid]["z_long_norm"] for uid in user_ids])), + "z_long_norm_std": float(np.std([all_vectors[uid]["z_long_norm"] for uid in user_ids])), + } + } + with open(method_dir / "user_vectors_summary.json", "w") as f: + json.dump(summary, f, indent=2) + + logger.info(f" Exported {len(user_ids)} user vectors to {npz_path}") + def run_single_session( self, method: str, @@ -297,11 +366,11 @@ class ExperimentRunner: # Structured preferences with condition/action pref_str = "\n".join([ f"- When {p.get('condition', '')}, {p.get('action', '')}" - for p in user_prefs[:10] # Top 10 preferences + for p in user_prefs ]) else: # Simple string preferences - pref_str = "\n".join([f"- {p}" for p in user_prefs[:10]]) + pref_str = "\n".join([f"- {p}" for p in user_prefs]) else: pref_str = str(user_prefs) @@ -619,6 +688,9 @@ class ExperimentRunner: json.dump(results, f, indent=2) logger.info(f" Profile {profile_idx + 1} completed and checkpointed") + # Export user vectors at the end of sequential processing + self._export_user_vectors(method, {0: adapter}) + return results def _run_method_parallel( @@ -690,6 +762,10 @@ class ExperimentRunner: except Exception as e: logger.error(f" Profile {profile_idx} failed: {e}") + # Note: Parallel mode doesn't export user vectors because adapters are + # created/destroyed per profile. Use batch mode for vector export. + logger.info(f" Parallel mode: user vectors not exported (use batch mode)") + def _run_method_batch( self, method: str, @@ -724,7 +800,7 @@ class ExperimentRunner: else: user_client = BatchVLLMClient( vllm_url=self.config.vllm_user_url, - max_tokens=4096, + max_tokens=1024, # User responses typically short, but allow for edge cases temperature=1.0, timeout=None, max_concurrent=100, @@ -799,21 +875,34 @@ class ExperimentRunner: adapters = {} profile_sessions = {} + # Build session problem list ONCE (shared across all profiles for controlled comparison) + # Each dataset contributes exactly n_per_dataset problems (front 10), no repeats + shared_sessions = [] + dataset_names = list(self.datasets.keys()) + n_per_dataset = self.config.n_sessions_per_profile // len(dataset_names) + remainder = self.config.n_sessions_per_profile % len(dataset_names) + + for i, ds_name in enumerate(dataset_names): + ds_obj = self.datasets[ds_name] + items = ds_obj.get_testset() + n_take = n_per_dataset + (1 if i < remainder else 0) + if n_take > len(items): + logger.warning(f" Dataset {ds_name} has only {len(items)} problems, need {n_take}") + for j in range(n_take): + item = items[j % len(items)] + shared_sessions.append({"problem": item.problem, "solution": item.solution, "domain": ds_obj.domain}) + + n_conflict = int(len(shared_sessions) * self.config.conflict_ratio) + shared_session_list = [(s, idx < n_conflict) for idx, s in enumerate(shared_sessions)] + logger.info(f" Built shared session list: {len(shared_sessions)} problems from {len(dataset_names)} datasets ({n_per_dataset} each, same for all profiles)") + for profile_idx in profiles_to_run: profile = self.profiles[profile_idx] adapter = self._create_method_adapter(method, profile, use_shared_models=True) if hasattr(adapter, 'initialize'): adapter.initialize() adapters[profile_idx] = adapter - - sessions = [] - for ds_name, ds_obj in self.datasets.items(): - ds_items = ds_obj.get_testset() - for item in ds_items[:self.config.n_sessions_per_profile]: - sessions.append({"problem": item.problem, "solution": item.solution, "domain": ds_obj.domain}) - sessions = sessions[:self.config.n_sessions_per_profile] - n_conflict = int(len(sessions) * self.config.conflict_ratio) - profile_sessions[profile_idx] = [(s, idx < n_conflict) for idx, s in enumerate(sessions)] + profile_sessions[profile_idx] = shared_session_list n_sessions = self.config.n_sessions_per_profile @@ -860,9 +949,9 @@ class ExperimentRunner: user_prefs = profile.get("preferences", []) if isinstance(user_prefs, list) and user_prefs: if isinstance(user_prefs[0], dict): - pref_str = "\n".join([f"- When {p.get('condition','')}, {p.get('action','')}" for p in user_prefs[:10]]) + pref_str = "\n".join([f"- When {p.get('condition','')}, {p.get('action','')}" for p in user_prefs]) else: - pref_str = "\n".join([f"- {p}" for p in user_prefs[:10]]) + pref_str = "\n".join([f"- {p}" for p in user_prefs]) else: pref_str = str(user_prefs) @@ -916,21 +1005,105 @@ class ExperimentRunner: state["conversation"].append({"role": "user", "content": user_msg}) state["full_log"].append(parsed) - if parsed.get("enforce_preferences", False): + enforce = parsed.get("enforce_preferences", False) + if isinstance(enforce, str): + enforce = enforce.lower() == "true" + if enforce: state["enforcement_count"] += 1 + # Detect disappointment and satisfaction from user message + # Disappointment indicators (not quite right, could be better, etc.) + user_msg_lower = user_msg.lower() + disappointment = any(phrase in user_msg_lower for phrase in [ + "not quite", "not what i", "that's not", "incorrect", + "wrong", "mistake", "error", "confused", "doesn't make sense", + "try again", "not helpful", "not useful" + ]) + # Satisfaction indicators (explicit positive feedback) + satisfaction = parsed.get("should_terminate", False) or any(phrase in user_msg_lower for phrase in [ + "perfect", "exactly", "great", "thanks", "helpful", + "that's right", "correct", "good job", "well done", + "makes sense", "understand now", "got it" + ]) + + # Store parsed feedback for REINFORCE (applied AFTER prepare_prompt sets pending_rl_update) + state["_pending_feedback"] = { + "user_msg": user_msg, + "enforce": bool(enforce), + "disappointment": disappointment and not enforce, # Don't double-count + "satisfaction": satisfaction and not enforce, # Don't count if also enforcing + "draft_answer": bool(parsed.get("draft_answer")), + } + if parsed.get("should_terminate", False) or TERMINATION_SIGNAL in user_msg: to_remove.append(pidx) continue - # Prepare agent prompt for batching (don't call LLM yet) + # Batch preference extraction for PersonalizedLLM adapters + extraction_batch = [] # (pidx, query) + remaining_active = [pidx for pidx in active_list if pidx not in to_remove] + for pidx in remaining_active: + adapter = adapters.get(pidx) + if adapter and hasattr(adapter, '_llm') and hasattr(adapter._llm, 'enable_preference_extraction'): + if adapter._llm.enable_preference_extraction and adapter._llm._extractor is not None: + query = adapter._llm.get_last_user_query(adapter._current_user_id) if hasattr(adapter._llm, 'get_last_user_query') else None + if not query: + state = all_states[pidx] + query = state["conversation"][-1]["content"] if state["conversation"] else "" + if query: + extraction_batch.append((pidx, query)) + + if extraction_batch: + extractor = extraction_batch[0][1] # just need any adapter to get the extractor + adapter0 = adapters[extraction_batch[0][0]] + shared_extractor = adapter0._llm._extractor + if hasattr(shared_extractor, 'batch_extract_preferences'): + queries = [q for _, q in extraction_batch] + batch_results = shared_extractor.batch_extract_preferences(queries) + for (pidx, _), pref_dict in zip(extraction_batch, batch_results): + adapter = adapters[pidx] + adapter._llm.apply_extracted_preferences(adapter._current_user_id, pref_dict) + else: + # Fallback: sequential + for pidx, query in extraction_batch: + adapter = adapters[pidx] + adapter._llm._extractor.extract_turn(adapter._llm._sessions[adapter._current_user_id].session_state.history) + + # Batch scaffolding for reflection adapters before prepare_prompt + scaffolding_batch = [] # (pidx, prompt) + remaining_active = [pidx for pidx in active_list if pidx not in to_remove] + for pidx in remaining_active: + adapter = adapters.get(pidx) + if adapter and hasattr(adapter, 'get_scaffolding_prompt'): + state = all_states[pidx] + # Temporarily add user msg to history for scaffolding + agent_notes = adapter._user_notes.get(adapter._current_user_id, "No notes yet about this user.") + if adapter.with_scaffolding and agent_notes != "No notes yet about this user.": + prompt = adapter.get_scaffolding_prompt( + state["conversation"], agent_notes) + if prompt is not None: + scaffolding_batch.append((pidx, prompt)) + + if scaffolding_batch: + scaff_messages = [[{"role": "user", "content": p}] for _, p in scaffolding_batch] + scaff_responses = agent_client.batch_completion(scaff_messages) + for (pidx, _), resp in zip(scaffolding_batch, scaff_responses): + adapter = adapters[pidx] + adapter._scaffolding_result = resp if resp else None + + # Prepare agent prompts for batching + # NOTE: prepare_prompt calls chat_prepare which sets pending_rl_update + # from the previous turn's data. REINFORCE feedback must be applied + # AFTER this call so that pending_rl_update is available. + for pidx in remaining_active: + state = all_states[pidx] try: adapter = adapters[pidx] + user_msg = state["conversation"][-1]["content"] if hasattr(adapter, 'prepare_prompt'): messages, context = adapter.prepare_prompt(user_msg, state["conversation"][:-1]) agent_prompts_batch.append((pidx, messages, context)) elif hasattr(adapter, 'generate_response'): - # Fallback for adapters without prepare_prompt agent_prompts_batch.append((pidx, None, None)) else: state["conversation"].append({"role": "assistant", "content": "[Error: Adapter not configured]"}) @@ -938,6 +1111,53 @@ class ExperimentRunner: logger.error(f" Agent prepare error p{pidx} t{turn}: {e}") state["conversation"].append({"role": "assistant", "content": "I apologize, I encountered an error. Could you rephrase?"}) + # Apply REINFORCE feedback NOW (after prepare_prompt set pending_rl_update) + for pidx in remaining_active: + state = all_states[pidx] + fb = state.pop("_pending_feedback", None) + if fb: + adapter = adapters.get(pidx) + if adapter and hasattr(adapter, 'process_user_turn'): + adapter.process_user_turn( + user_response=fb["user_msg"], + enforce_preferences=fb["enforce"], + express_disappointment=fb.get("disappointment", False), + express_satisfaction=fb["satisfaction"], + draft_answer_updated=fb["draft_answer"], + ) + + # Also apply feedback for terminated sessions (they skipped prepare_prompt + # but still need the reward signal from their last turn) + for pidx in to_remove: + state = all_states.get(pidx) + if not state: + continue + fb = state.pop("_pending_feedback", None) + if fb: + adapter = adapters.get(pidx) + if adapter and hasattr(adapter, 'process_user_turn'): + # For terminated sessions, we can't call prepare_prompt + # (no next turn), but we still want the reward applied. + # Call chat_prepare with a dummy to set pending_rl_update, + # then apply feedback. + try: + if hasattr(adapter, '_llm') and hasattr(adapter._llm, 'chat_prepare'): + adapter._llm.chat_prepare( + adapter._current_user_id, + fb["user_msg"], + skip_extraction=True, + skip_auto_reward=True, + ) + adapter.process_user_turn( + user_response=fb["user_msg"], + enforce_preferences=fb["enforce"], + express_disappointment=fb.get("disappointment", False), + express_satisfaction=fb["satisfaction"], + draft_answer_updated=fb["draft_answer"], + ) + except Exception: + pass # Best effort for terminated sessions + # Batch vLLM call for all agent prompts if agent_prompts_batch: # Separate prompts that can be batched from fallback @@ -979,6 +1199,25 @@ class ExperimentRunner: active_set -= set(to_remove) + # Batch note-update for reflection adapters before end_session + note_update_batch = [] # (profile_idx, messages) + for profile_idx in profiles_to_run: + if profile_idx not in all_states: + continue + adapter = adapters.get(profile_idx) + if adapter and hasattr(adapter, 'get_note_update_prompt'): + prompt_msgs = adapter.get_note_update_prompt() + if prompt_msgs is not None: + note_update_batch.append((profile_idx, prompt_msgs)) + + if note_update_batch: + note_messages = [msgs for _, msgs in note_update_batch] + note_responses = agent_client.batch_completion(note_messages) + for (profile_idx, _), resp in zip(note_update_batch, note_responses): + if resp: + adapter = adapters[profile_idx] + adapter.apply_note_update_response(resp) + # Save results for this session round for profile_idx in profiles_to_run: if profile_idx not in all_states: @@ -995,10 +1234,20 @@ class ExperimentRunner: task_success = 0 for entry in full_log: if entry.get("should_terminate", False): - draft = entry.get("draft_answer", "") - if draft and "don't know" not in draft.lower() and len(draft) > 20: + draft = str(entry.get("draft_answer", "")) + if draft and "don't know" not in draft.lower(): task_success = 1 + # End session on adapter (applies task completion reward for REINFORCE) + adapter = adapters.get(profile_idx) + if adapter and hasattr(adapter, 'end_session'): + # Skip note update if batch already handled it + skip_notes = hasattr(adapter, 'get_note_update_prompt') + try: + adapter.end_session(task_success=bool(task_success), skip_note_update=skip_notes) + except TypeError: + adapter.end_session(task_success=bool(task_success)) + results.append({ "method": method, "profile_id": self.profiles[profile_idx].get("user_id", f"user_{profile_idx}"), @@ -1023,6 +1272,33 @@ class ExperimentRunner: "adapter_metrics": {}, }) + # Collect adapter metrics (e.g. user_vector_norm for rag_vector) + adapter = adapters.get(profile_idx) + if adapter and hasattr(adapter, 'get_user_vector'): + user_id = self.profiles[profile_idx].get("user_id", f"user_{profile_idx}") + vec = adapter.get_user_vector(user_id) + if vec is not None: + results[-1]["adapter_metrics"] = { + "user_vector_norm": float(np.linalg.norm(vec)), + } + + # Save user vector snapshots every 10 sessions + if (session_idx + 1) % 10 == 0: + vectors_dir = checkpoint_file.parent / "vectors" + vectors_dir.mkdir(parents=True, exist_ok=True) + user_vectors = {} + for profile_idx in profiles_to_run: + adapter = adapters.get(profile_idx) + if adapter and hasattr(adapter, 'get_user_vector'): + user_id = self.profiles[profile_idx].get("user_id", f"user_{profile_idx}") + vec = adapter.get_user_vector(user_id) + if vec is not None: + user_vectors[user_id] = vec + if user_vectors: + snapshot_path = vectors_dir / f"vectors_session_{session_idx+1}.npy" + np.save(snapshot_path, user_vectors) + logger.info(f" Saved {len(user_vectors)} user vectors to {snapshot_path}") + # Checkpoint after each session round with session-level tracking # Only increment for profiles that actually ran in this round (those in all_states) for profile_idx in all_states.keys(): @@ -1043,6 +1319,9 @@ class ExperimentRunner: rate = sessions_done / elapsed * 3600 if elapsed > 0 else 0 logger.info(f" Session round {session_idx+1}/{n_sessions}: {sessions_done} total, {rate:.0f} sessions/hr") + # Export user vectors before cleanup (for RAG methods with user vectors) + self._export_user_vectors(method, adapters) + # Explicitly free adapter models to prevent GPU OOM across methods for pidx, adapter in adapters.items(): if hasattr(adapter, 'cleanup'): diff --git a/collaborativeagents/scripts/test_new_rewrite.sh b/collaborativeagents/scripts/test_new_rewrite.sh new file mode 100755 index 0000000..1ade8ea --- /dev/null +++ b/collaborativeagents/scripts/test_new_rewrite.sh @@ -0,0 +1,50 @@ +#!/bin/bash +# 小规模测试:验证新的rewrite prompt是否降低E/T +# 10 profiles × 10 sessions = 100 sessions + +echo "$(date '+%H:%M:%S') 启动 rag_rewrite 小规模测试 (新prompt)..." + +cd /workspace/personalization-user-model + +python collaborativeagents/scripts/run_experiments.py \ + --methods rag_rewrite \ + --datasets math-hard,bigcodebench \ + --n-profiles 10 \ + --n-sessions 10 \ + --max-turns 10 \ + --use-vllm \ + --vllm-agent-url http://localhost:8003/v1 \ + --vllm-user-url http://localhost:8004/v1 \ + --use-batch-processing \ + --batch-size 4 \ + --parallel-profiles 10 \ + --profile-path collaborativeagents/data/complex_profiles_v2/profiles_200.jsonl \ + --output-dir collaborativeagents/results/test_new_rewrite_10x10 + +echo "$(date '+%H:%M:%S') 测试完成" + +# 自动分析结果 +python3 << 'ANALYZE' +import json +import numpy as np + +result_path = "collaborativeagents/results/test_new_rewrite_10x10" +import glob +results_file = glob.glob(f"{result_path}/*/rag_rewrite/results.json") + +if results_file: + with open(results_file[0]) as f: + data = json.load(f) + + enforcements = sum(r["metrics"]["enforcement_count"] for r in data) + turns = sum(r["metrics"]["total_turns"] for r in data) + successes = sum(1 for r in data if r["metrics"]["task_success"]) + + print(f"\n=== 新Rewrite Prompt测试结果 ===") + print(f"Sessions: {len(data)}") + print(f"Success Rate: {100*successes/len(data):.1f}%") + print(f"E/T: {enforcements/turns:.4f}") + print(f"(对比旧rewrite E/T: 0.194)") +else: + print("结果文件未找到") +ANALYZE diff --git a/collaborativeagents/scripts/visualize_user_vectors.py b/collaborativeagents/scripts/visualize_user_vectors.py new file mode 100644 index 0000000..203cb68 --- /dev/null +++ b/collaborativeagents/scripts/visualize_user_vectors.py @@ -0,0 +1,407 @@ +#!/usr/bin/env python3 +""" +User Vector Visualization Script + +Visualizes learned user vectors using t-SNE and PCA for dimensionality reduction. +Supports multiple coloring schemes to analyze user clusters. + +Usage: + python visualize_user_vectors.py --results-dir ../results/fullrun_3methods + python visualize_user_vectors.py --vectors-file user_vectors.npy --profiles-file profiles.json +""" + +import argparse +import json +import numpy as np +import matplotlib.pyplot as plt +from pathlib import Path +from typing import Dict, List, Optional, Tuple +from sklearn.manifold import TSNE +from sklearn.decomposition import PCA +from sklearn.preprocessing import StandardScaler +import warnings +warnings.filterwarnings('ignore') + + +def load_user_vectors(results_dir: Path) -> Tuple[np.ndarray, List[int]]: + """Load user vectors from experiment results.""" + vectors = [] + user_ids = [] + + # Try to find user vectors in different locations + possible_paths = [ + results_dir / "user_vectors.npy", + results_dir / "rag_vector" / "user_vectors.npy", + results_dir / "checkpoints" / "user_vectors.npy", + ] + + for path in possible_paths: + if path.exists(): + data = np.load(path, allow_pickle=True) + if isinstance(data, np.ndarray): + if data.dtype == object: + # Dictionary format + data = data.item() + for uid, vec in data.items(): + user_ids.append(int(uid)) + vectors.append(vec) + else: + # Direct array format + vectors = data + user_ids = list(range(len(data))) + print(f"Loaded {len(vectors)} user vectors from {path}") + return np.array(vectors), user_ids + + # Try to extract from results.json + results_files = list(results_dir.glob("**/results.json")) + for rf in results_files: + try: + with open(rf) as f: + data = json.load(f) + # Extract user vectors if stored in results + if isinstance(data, dict) and "user_vectors" in data: + for uid, vec in data["user_vectors"].items(): + user_ids.append(int(uid)) + vectors.append(np.array(vec)) + print(f"Loaded {len(vectors)} user vectors from {rf}") + return np.array(vectors), user_ids + except: + continue + + raise FileNotFoundError(f"No user vectors found in {results_dir}") + + +def load_profiles(profiles_path: Path) -> List[Dict]: + """Load user profiles for labeling.""" + if profiles_path.suffix == '.jsonl': + profiles = [] + with open(profiles_path) as f: + for line in f: + profiles.append(json.loads(line)) + return profiles + else: + with open(profiles_path) as f: + return json.load(f) + + +def extract_profile_features(profiles: List[Dict]) -> Dict[str, List]: + """Extract features from profiles for coloring.""" + features = { + "categories": [], + "n_preferences": [], + "persona_length": [], + } + + for p in profiles: + # Extract categories if available + cats = p.get("categories", []) + features["categories"].append(cats[0] if cats else "unknown") + + # Number of preferences + prefs = p.get("preferences", []) + features["n_preferences"].append(len(prefs)) + + # Persona length + persona = p.get("persona", "") + features["persona_length"].append(len(persona)) + + return features + + +def apply_tsne(vectors: np.ndarray, perplexity: int = 30, max_iter: int = 1000) -> np.ndarray: + """Apply t-SNE dimensionality reduction.""" + # Standardize vectors + scaler = StandardScaler() + vectors_scaled = scaler.fit_transform(vectors) + + # Adjust perplexity if needed + n_samples = len(vectors) + perplexity = min(perplexity, n_samples - 1) + + tsne = TSNE( + n_components=2, + perplexity=perplexity, + max_iter=max_iter, + random_state=42, + init='pca', + learning_rate='auto' + ) + return tsne.fit_transform(vectors_scaled) + + +def apply_pca(vectors: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: + """Apply PCA dimensionality reduction. Returns (2D projection, explained variance).""" + scaler = StandardScaler() + vectors_scaled = scaler.fit_transform(vectors) + + pca = PCA(n_components=min(10, vectors.shape[1])) + transformed = pca.fit_transform(vectors_scaled) + + return transformed[:, :2], pca.explained_variance_ratio_ + + +def plot_comparison( + vectors: np.ndarray, + user_ids: List[int], + profiles: Optional[List[Dict]] = None, + output_path: Optional[Path] = None, + title_prefix: str = "" +): + """Create side-by-side t-SNE and PCA plots.""" + + # Apply dimensionality reduction + print("Applying t-SNE...") + tsne_2d = apply_tsne(vectors) + + print("Applying PCA...") + pca_2d, pca_variance = apply_pca(vectors) + + # Prepare coloring + if profiles and len(profiles) >= len(user_ids): + features = extract_profile_features(profiles) + color_by = features["n_preferences"] + color_label = "Number of Preferences" + else: + color_by = user_ids + color_label = "User ID" + + # Create figure + fig, axes = plt.subplots(1, 2, figsize=(16, 7)) + + # t-SNE plot + ax1 = axes[0] + scatter1 = ax1.scatter( + tsne_2d[:, 0], tsne_2d[:, 1], + c=color_by, cmap='viridis', alpha=0.7, s=50 + ) + ax1.set_xlabel('t-SNE Dimension 1') + ax1.set_ylabel('t-SNE Dimension 2') + ax1.set_title(f'{title_prefix}t-SNE Visualization\n({len(vectors)} users)') + plt.colorbar(scatter1, ax=ax1, label=color_label) + + # PCA plot + ax2 = axes[1] + scatter2 = ax2.scatter( + pca_2d[:, 0], pca_2d[:, 1], + c=color_by, cmap='viridis', alpha=0.7, s=50 + ) + ax2.set_xlabel(f'PC1 ({pca_variance[0]*100:.1f}% variance)') + ax2.set_ylabel(f'PC2 ({pca_variance[1]*100:.1f}% variance)') + ax2.set_title(f'{title_prefix}PCA Visualization\n(Top 2 components: {(pca_variance[0]+pca_variance[1])*100:.1f}% variance)') + plt.colorbar(scatter2, ax=ax2, label=color_label) + + plt.tight_layout() + + if output_path: + plt.savefig(output_path, dpi=150, bbox_inches='tight') + print(f"Saved comparison plot to {output_path}") + + plt.show() + + return tsne_2d, pca_2d, pca_variance + + +def plot_by_category( + vectors: np.ndarray, + user_ids: List[int], + profiles: List[Dict], + output_path: Optional[Path] = None +): + """Create plots colored by preference category.""" + + features = extract_profile_features(profiles) + categories = features["categories"] + unique_cats = list(set(categories)) + cat_to_idx = {c: i for i, c in enumerate(unique_cats)} + cat_colors = [cat_to_idx[c] for c in categories[:len(user_ids)]] + + # Apply reductions + tsne_2d = apply_tsne(vectors) + pca_2d, pca_variance = apply_pca(vectors) + + fig, axes = plt.subplots(1, 2, figsize=(16, 7)) + + # t-SNE by category + ax1 = axes[0] + scatter1 = ax1.scatter( + tsne_2d[:, 0], tsne_2d[:, 1], + c=cat_colors, cmap='tab10', alpha=0.7, s=50 + ) + ax1.set_xlabel('t-SNE Dimension 1') + ax1.set_ylabel('t-SNE Dimension 2') + ax1.set_title('t-SNE by Preference Category') + + # PCA by category + ax2 = axes[1] + scatter2 = ax2.scatter( + pca_2d[:, 0], pca_2d[:, 1], + c=cat_colors, cmap='tab10', alpha=0.7, s=50 + ) + ax2.set_xlabel(f'PC1 ({pca_variance[0]*100:.1f}%)') + ax2.set_ylabel(f'PC2 ({pca_variance[1]*100:.1f}%)') + ax2.set_title('PCA by Preference Category') + + # Add legend + handles = [plt.scatter([], [], c=[cat_to_idx[c]], cmap='tab10', label=c) + for c in unique_cats[:10]] # Limit to 10 categories + fig.legend(handles, unique_cats[:10], loc='center right', title='Category') + + plt.tight_layout() + plt.subplots_adjust(right=0.85) + + if output_path: + plt.savefig(output_path, dpi=150, bbox_inches='tight') + print(f"Saved category plot to {output_path}") + + plt.show() + + +def plot_pca_variance(vectors: np.ndarray, output_path: Optional[Path] = None): + """Plot PCA explained variance to understand dimensionality.""" + scaler = StandardScaler() + vectors_scaled = scaler.fit_transform(vectors) + + n_components = min(50, vectors.shape[1], vectors.shape[0]) + pca = PCA(n_components=n_components) + pca.fit(vectors_scaled) + + fig, axes = plt.subplots(1, 2, figsize=(14, 5)) + + # Individual variance + ax1 = axes[0] + ax1.bar(range(1, n_components + 1), pca.explained_variance_ratio_ * 100) + ax1.set_xlabel('Principal Component') + ax1.set_ylabel('Explained Variance (%)') + ax1.set_title('PCA Explained Variance by Component') + ax1.set_xlim(0, n_components + 1) + + # Cumulative variance + ax2 = axes[1] + cumvar = np.cumsum(pca.explained_variance_ratio_) * 100 + ax2.plot(range(1, n_components + 1), cumvar, 'b-o', markersize=4) + ax2.axhline(y=90, color='r', linestyle='--', label='90% variance') + ax2.axhline(y=95, color='g', linestyle='--', label='95% variance') + ax2.set_xlabel('Number of Components') + ax2.set_ylabel('Cumulative Explained Variance (%)') + ax2.set_title('PCA Cumulative Explained Variance') + ax2.legend() + ax2.set_xlim(0, n_components + 1) + ax2.set_ylim(0, 105) + + # Find components needed for 90% and 95% variance + n_90 = np.argmax(cumvar >= 90) + 1 + n_95 = np.argmax(cumvar >= 95) + 1 + print(f"Components for 90% variance: {n_90}") + print(f"Components for 95% variance: {n_95}") + + plt.tight_layout() + + if output_path: + plt.savefig(output_path, dpi=150, bbox_inches='tight') + print(f"Saved variance plot to {output_path}") + + plt.show() + + return pca.explained_variance_ratio_ + + +def generate_synthetic_vectors(n_users: int = 200, dim: int = 64) -> np.ndarray: + """Generate synthetic user vectors for testing visualization.""" + np.random.seed(42) + + # Create 5 clusters of users + n_clusters = 5 + cluster_size = n_users // n_clusters + vectors = [] + + for i in range(n_clusters): + # Each cluster has a different center + center = np.random.randn(dim) * 2 + # Users in cluster are variations around center + cluster_vectors = center + np.random.randn(cluster_size, dim) * 0.5 + vectors.append(cluster_vectors) + + # Add remaining users + remaining = n_users - n_clusters * cluster_size + if remaining > 0: + vectors.append(np.random.randn(remaining, dim)) + + return np.vstack(vectors) + + +def main(): + parser = argparse.ArgumentParser(description="Visualize user vectors with t-SNE and PCA") + parser.add_argument("--results-dir", type=str, help="Path to experiment results directory") + parser.add_argument("--vectors-file", type=str, help="Path to user vectors .npy file") + parser.add_argument("--profiles-file", type=str, help="Path to user profiles JSON file") + parser.add_argument("--output-dir", type=str, default=".", help="Output directory for plots") + parser.add_argument("--demo", action="store_true", help="Run demo with synthetic data") + args = parser.parse_args() + + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + if args.demo: + print("Running demo with synthetic user vectors...") + vectors = generate_synthetic_vectors(200, 64) + user_ids = list(range(200)) + profiles = None + title_prefix = "[Demo] " + elif args.vectors_file: + vectors = np.load(args.vectors_file) + user_ids = list(range(len(vectors))) + profiles = None + if args.profiles_file: + profiles = load_profiles(Path(args.profiles_file)) + title_prefix = "" + elif args.results_dir: + results_dir = Path(args.results_dir) + vectors, user_ids = load_user_vectors(results_dir) + + # Try to find profiles + profiles = None + profile_paths = [ + results_dir / "generated_profiles.json", + results_dir.parent / "profiles.json", + Path("../data/complex_profiles_v2/profiles_200.jsonl"), + ] + for pp in profile_paths: + if pp.exists(): + profiles = load_profiles(pp) + print(f"Loaded {len(profiles)} profiles from {pp}") + break + title_prefix = "" + else: + print("Please provide --results-dir, --vectors-file, or --demo") + return + + print(f"\nUser vectors shape: {vectors.shape}") + print(f"Number of users: {len(user_ids)}") + + # Generate plots + print("\n=== Generating comparison plot ===") + plot_comparison( + vectors, user_ids, profiles, + output_path=output_dir / "user_vectors_comparison.png", + title_prefix=title_prefix + ) + + print("\n=== Generating PCA variance plot ===") + plot_pca_variance( + vectors, + output_path=output_dir / "user_vectors_pca_variance.png" + ) + + if profiles and len(profiles) >= len(user_ids): + print("\n=== Generating category plot ===") + plot_by_category( + vectors, user_ids, profiles, + output_path=output_dir / "user_vectors_by_category.png" + ) + + print("\n=== Done! ===") + print(f"Plots saved to {output_dir}") + + +if __name__ == "__main__": + main() diff --git a/configs/local_models.yaml b/configs/local_models.yaml index 8f91955..9372f3d 100644 --- a/configs/local_models.yaml +++ b/configs/local_models.yaml @@ -1,19 +1,19 @@ # Base path for all models -_base_path: &base /projects/bfqt/users/yurenh2/ml-projects/personalization-user-model +_base_path: &base /workspace/personalization-user-model models: llm: # New Multi-Backend Config qwen_1_5b: backend: qwen - path: /projects/bfqt/users/yurenh2/ml-projects/personalization-user-model/models/qwen2.5-1.5b-instruct + path: /workspace/personalization-user-model/models/qwen2.5-1.5b-instruct device: auto dtype: bfloat16 max_context_length: 4096 llama_8b: backend: llama - path: /projects/bfqt/users/yurenh2/ml-projects/personalization-user-model/models/llama-3.1-8b-instruct + path: /workspace/personalization-user-model/models/llama-3.1-8b-instruct device: auto dtype: bfloat16 max_context_length: 8192 @@ -21,13 +21,13 @@ models: # vLLM backend for high-throughput experiments llama_8b_vllm: backend: vllm - path: /projects/bfqt/users/yurenh2/ml-projects/personalization-user-model/models/llama-3.1-8b-instruct + path: /workspace/personalization-user-model/models/llama-3.1-8b-instruct vllm_url: http://localhost:8003/v1 max_context_length: 8192 # Legacy fallback (needed if from_config is called directly without name) hf_id: Qwen/Qwen2.5-1.5B-Instruct - local_path: /projects/bfqt/users/yurenh2/ml-projects/personalization-user-model/models/qwen2.5-1.5b-instruct + local_path: /workspace/personalization-user-model/models/qwen2.5-1.5b-instruct dtype: bfloat16 device_map: auto @@ -35,12 +35,12 @@ models: # Default/Legacy default: hf_id: Qwen/Qwen2.5-0.5B-Instruct - local_path: /projects/bfqt/users/yurenh2/ml-projects/personalization-user-model/models/qwen2.5-0.5b-instruct + local_path: /workspace/personalization-user-model/models/qwen2.5-0.5b-instruct dtype: bfloat16 device_map: auto # New SFT Extractor qwen3_0_6b_sft: - path: /projects/bfqt/users/yurenh2/ml-projects/personalization-user-model/saves/qwen3-0.6b-full-sft-h200/checkpoint-4358 + path: /workspace/personalization-user-model/models/pref-extractor-qwen3-0.6b-sft prompt_template_path: fine_tuning_prompt_template.txt device: auto dtype: bfloat16 @@ -48,18 +48,18 @@ models: embedding: qwen3: hf_id: Qwen/Qwen3-Embedding-8B - local_path: /projects/bfqt/users/yurenh2/ml-projects/personalization-user-model/models/qwen3-embedding-8b + local_path: /workspace/personalization-user-model/models/qwen3-embedding-8b nemotron: hf_id: nvidia/llama-embed-nemotron-8b - local_path: /projects/bfqt/users/yurenh2/ml-projects/personalization-user-model/models/llama-embed-nemotron-8b + local_path: /workspace/personalization-user-model/models/llama-embed-nemotron-8b reranker: qwen3_8b: hf_id: Qwen/Qwen3-Reranker-8B - local_path: /projects/bfqt/users/yurenh2/ml-projects/personalization-user-model/models/rerankers/qwen3-reranker-8b + local_path: /workspace/personalization-user-model/models/rerankers/qwen3-reranker-8b dtype: bfloat16 device_map: auto bge_base: hf_id: BAAI/bge-reranker-base - local_path: /projects/bfqt/users/yurenh2/ml-projects/personalization-user-model/models/rerankers/bge-reranker-base + local_path: /workspace/personalization-user-model/models/rerankers/bge-reranker-base dtype: float16 device_map: auto diff --git a/docs/rag_improvement_ideas.md b/docs/rag_improvement_ideas.md new file mode 100644 index 0000000..f49a6b3 --- /dev/null +++ b/docs/rag_improvement_ideas.md @@ -0,0 +1,238 @@ +# RAG Retrieval Improvement Ideas + +> 记录日期: 2026-02-07 +> 当前状态: RAG方法成功率52.2%,超时率41.4%,E/T 0.193 +> 目标: 降低超时率至接近Reflection的25.9% + +## 与Reflection Baseline的差距分析 + +| 指标 | RAG (topk5) | Reflection | 差距 | +|------|-------------|------------|------| +| 成功率 | 52.2% | 45.2% | +7.0% ✓ 领先 | +| 超时率 | 41.4% | 25.9% | +15.5% ✗ 落后 | +| E/T | 0.193 | 0.162 | +0.031 ✗ 落后 | + +**核心问题**: 超时率高,agent未能在max_turns内满足用户偏好 + +--- + +## A. 检索质量优化 (Retrieval) + +### A1. Query Expansion ✅ 已实现 +- **状态**: 已实现,enable_query_transform=True +- **当前方案**: 检测任务类型(math/coding/writing/explanation),添加前缀 "user preferences for {type} tasks" +- **Bug修复**: 2026-02-07 修复了词边界匹配问题("api"误匹配"capital") +- **待验证**: 需要实验验证效果 + +### A2. Preference Embedding优化 +- **问题**: 当前直接embed "When X, do Y" 格式的完整句子 +- **改进方案**: + - 同时embed多个变体: action-only, condition-only, paraphrased + - 存储时创建多个embedding,检索时取最高分 +- **实现难度**: 中 +- **预期收益**: 提高检索召回率 + +### A3. 动态TopK +- **问题**: 固定topk可能过多(引入噪声)或过少(遗漏重要偏好) +- **实验结果**: + - topk=3: 成功率42.2%, 超时率57.7% ❌ 更差 + - topk=5: 成功率52.2%, 超时率41.4% (基线) + - topk=8: 实验中... +- **改进方案**: 基于rerank分数动态决定 + - 如果top-1分数 > 0.8,只取top-3 + - 如果top-1分数 < 0.5,取top-8 +- **实现难度**: 低 +- **预期收益**: 中 + +### A4. Negative Filtering +- **问题**: reranker可能给不相关偏好较高分数 +- **改进方案**: + - 添加分数阈值,过滤低于阈值的偏好 + - 或训练一个二分类器判断相关性 +- **实现难度**: 低-中 +- **预期收益**: 减少噪声干扰 + +--- + +## B. Prompt Engineering + +### B1. 偏好优先级标注 ⭐ 高优先级 +- **问题**: 所有偏好平等列出,模型不知道哪个更相关 +- **改进方案**: + ``` + ## Highly Relevant Preferences (rerank > 0.7) + - [preference 1] + - [preference 2] + + ## Possibly Relevant Preferences + - [preference 3] + ``` +- **实现难度**: 低(只改prompt构建逻辑) +- **预期收益**: 中-高 + +### B2. 偏好分类呈现 +- **问题**: 混合不同类型的偏好可能造成混淆 +- **改进方案**: + ``` + ## Format Preferences + - Use bullet points + - Include code blocks with language + + ## Content Preferences + - Show step-by-step derivation + - Provide examples + ``` +- **实现难度**: 中(需要偏好分类器) +- **预期收益**: 中 + +### B3. 显式推理步骤 ⭐ 高优先级 +- **Reflection的优势**: 强制模型输出 `user_preferences_reasoning` 字段 +- **改进方案**: + ``` + Before responding, briefly consider: + 1. Which of the above preferences are relevant to this request? + 2. How will you satisfy each relevant preference? + Then provide your response. + ``` +- **实现难度**: 低(只改prompt) +- **预期收益**: 高 + +### B4. 偏好Checklist验证 ⭐ 高优先级 +- **问题**: 模型可能忘记某些偏好 +- **改进方案**: + ``` + # Response Checklist + Before finalizing your response, verify: + □ [preference 1 - brief description] + □ [preference 2 - brief description] + ``` +- **实现难度**: 低 +- **预期收益**: 中-高 + +### B5. Few-shot示例 +- **改进方案**: 在prompt中添加一个"正确遵循偏好"的示例 +- **实现难度**: 低 +- **预期收益**: 中 +- **风险**: 增加context长度 + +--- + +## C. 偏好提取优化 (Extraction) + +### C1. 更细粒度的偏好 +- **问题**: 当前偏好可能太粗略 "When coding, use Python" +- **改进方案**: 分解为多个具体偏好 + - 编程语言: Python + - 代码格式: 单个可复制块 + - 注释风格: 内联注释 +- **实现难度**: 中(需要改extractor prompt) +- **预期收益**: 提高检索精度 + +### C2. 偏好置信度利用 +- **问题**: 低置信度偏好可能是噪声 +- **改进方案**: + - 存储时记录置信度 + - 检索时作为额外权重 +- **实现难度**: 低 +- **预期收益**: 低-中 + +### C3. 冲突检测 +- **问题**: 用户可能有矛盾的偏好(如"简洁" vs "详细") +- **改进方案**: + - 检测语义矛盾 + - 保留更新的偏好 + - 或在prompt中明确指出冲突 +- **实现难度**: 高 +- **预期收益**: 中 + +--- + +## D. 反馈循环优化 (Feedback Loop) + +### D1. 违规偏好标记 +- **问题**: 不知道哪个偏好导致用户enforce +- **改进方案**: + - 当用户enforce时,用小模型分析是哪个偏好被违反 + - 下次检索时boost该偏好权重 +- **实现难度**: 中-高 +- **预期收益**: 高 + +### D2. 会话内Correction记忆 ⭐ 中优先级 +- **问题**: 同一session内用户的correction可能被忽略 +- **改进方案**: + - 记录用户的每次correction + - 在后续turn中作为高优先级偏好 + - 临时存储,session结束后清除 +- **实现难度**: 中 +- **预期收益**: 中-高 + +### D3. 偏好验证 (Self-Critique) +- **问题**: 模型可能生成不符合偏好的响应 +- **改进方案**: + - 生成后用小模型/规则检查是否符合偏好 + - 不符合则重新生成或修正 +- **实现难度**: 高 +- **预期收益**: 高 +- **风险**: 增加延迟 + +--- + +## E. 架构改进 + +### E1. 两阶段生成 +- **方案**: + 1. 阶段1: 生成内容大纲/要点 + 2. 阶段2: 根据偏好润色格式 +- **实现难度**: 高 +- **预期收益**: 未知 + +### E2. 混合方法 (Reflection + RAG) +- **方案**: + - 使用Reflection的session-end notes作为长期记忆 + - 使用RAG检索相关偏好作为短期记忆 + - 两者结合注入prompt +- **实现难度**: 高 +- **预期收益**: 未知(可能是最优解) + +--- + +## 实验记录 + +### 已完成实验 + +| 实验名 | 配置 | 成功率 | 超时率 | E/T | 备注 | +|--------|------|--------|--------|-----|------| +| rag_prompt_priority | topk=5, prompt改进 | 52.2% | 41.4% | 0.193 | 基线 | +| rag_topk3_test | topk=3 | 42.2% | 57.7% | 0.191 | ❌ 更差 | + +### 进行中实验 + +| 实验名 | 配置 | 状态 | +|--------|------|------| +| rag_topk8_test | topk=8 | 运行中 | + +--- + +## 快速实验建议 + +按实现难度和预期收益排序: + +1. **[B3] 显式推理步骤** - 只改prompt,高收益 +2. **[B4] 偏好Checklist** - 只改prompt,中-高收益 +3. **[B1] 偏好优先级标注** - 小改动,中-高收益 +4. **[D2] 会话内correction记忆** - 中等改动,中-高收益 +5. **[A3] 动态TopK** - 小改动,中收益 + +--- + +## 参考: Reflection vs RAG 对比 + +| 特性 | Reflection | RAG (我们的) | +|------|------------|-------------| +| 偏好呈现 | 自然语言notes | 结构化bullet list | +| 推理步骤 | ✓ 显式 user_preferences_reasoning | ✗ 无 | +| 输出格式 | JSON with reasoning | 纯文本 | +| 反馈循环 | session-end reflection | 实时extraction | +| 检索方式 | LLM-based (proper_scaffolding) | Dense + Rerank | + +**关键差异**: Reflection强制模型在响应前显式推理如何满足偏好 diff --git a/notes.md b/notes.md new file mode 100644 index 0000000..be4e7de --- /dev/null +++ b/notes.md @@ -0,0 +1,385 @@ +# Personalization Experiments Notes + +## 已完成实验结果 + +### 30 Sessions (60 profiles) + +| 方法 | Success | E/T | Early(0-9) | Late(20-29) | 学习斜率 | +|------|---------|-----|------------|-------------|----------| +| reflection_v2 | 43.3% | 0.179 | 36.7% | 56.5% | +19.8% | +| rag_topk5_v3 | 41.3% | 0.188 | 31.2% | 58.0% | +26.8% | +| **rag_vector_v3** | **43.8%** | 0.188 | 38.5% | **59.2%** | +20.7% | + +**关键发现**: +- rag_vector总成功率最高 (43.8% > reflection 43.3%) +- 后期表现: rag_vector 59.2% > reflection 56.5% +- RAG学习斜率更大 (+26.8% vs +19.8%) + +### 60 Sessions (60 profiles) - ✅ 完成 02/10 00:55 + +**最终结果**: + +| 方法 | Success | Timeout | E/T | Early(0-9) | Late(50-59) | Slope | +|------|---------|---------|-----|------------|-------------|-------| +| reflection_60s | **40.3%** | 29.5% | **0.179** | **38.0%** | 43.5% | +5.5% | +| rag_vector_60s | 37.1% | 27.3% | 0.192 | 35.0% | 40.0% | +5.0% | +| rag_60s | 36.4% | **25.8%** | 0.192 | 33.8% | **42.5%** | **+8.7%** | + +**关键观察**: +1. **rag_vector > rag** (37.1% vs 36.4%),vector帮助了0.7% +2. **reflection仍然领先** (40.3%),差距3.1% +3. **RAG timeout更低** (25.8-27.3% vs 29.5%) +4. **rag学习斜率最高** (+8.7%) +5. **直接对比**: rag_vector赢733 vs rag赢708 (净+25) + +**按数据集**: +| 数据集 | reflection | rag | rag_vector | +|--------|------------|-----|------------| +| math | 35.4% | 30.7% | 30.9% | +| bigcodebench | 48.7% | 46.1% | 47.5% | + +### Full Scale (200 profiles × 60 sessions) - 旧版 + +| 方法 | Success | Code | Math | +|------|---------|------|------| +| vanilla | 52.8% | 73.1% | 39.2% | +| reflection | 54.0% | 73.2% | 41.2% | +| rag | 40.2% | 56.4% | 29.4% | +| rag_vector | 40.6% | 56.3% | 30.2% | + +**注意**: Full scale用的是旧版代码,没有query_transform和global_preferences优化 + +--- + +## Vector-Preference 相关性分析 + +### 30s实验 (rag_vector_v3) + +**Vector统计**: +- 59/60 users有非零vector +- Norm: mean=0.0023, max=0.0051 +- Vector随session增长: 0.00086 (s10) → 0.00227 (s30) + +**Jaccard(revealed_prefs) vs Vector相似度**: +- Spearman r = 0.09 (p=0.015) **显著** +- 趋势正确: 更多shared preferences → 更高vector相似度 + +| Jaccard范围 | 用户对数 | 平均Vector相似度 | +|-------------|---------|------------------| +| 0.00-0.05 | 413 | -0.007 | +| 0.05-0.10 | 215 | 0.034 | +| 0.10-0.15 | 43 | 0.037 | +| 0.15-0.25 | 10 | 0.072 | + +**对比之前论文**: r=0.90 (5 users, 20 sessions) vs 当前 r=0.09 (60 users, 30 sessions) + +--- + +## RAG vs Reflection 分析 + +### 为什么RAG不如Reflection? + +1. **检索方式不同**: + - Reflection: LLM-based筛选 (proper_scaffolding) + - RAG: Dense embedding + reranker + +2. **Notes格式不同**: + - Reflection: LLM反思总结,简洁coherent + - RAG: 原始extracted preferences,可能冗长 + +3. **Case分析** (60s实验): + - Reflection赢: 827 cases + - RAG赢: 686 cases + - Math上差距大: Reflection +117 + - Code上差距小: Reflection +24 + +### 典型失败Case +- RAG检索到更多细节preferences(如"JSON format") +- Agent无法同时遵守所有preferences +- 用户反复enforce → 超时/失败 + +--- + +## 改进方案 + +### 已实现: rag_rewrite + +**原理**: 用LLM合并多个preferences为1-2条简洁指令 + +**流程**: +``` +RAG检索 top-5 preferences + ↓ +LLM rewrite: 合并+简化 + ↓ +1条简洁指令注入prompt +``` + +**Prompt关键点**: +- 温度0.3(稳定输出) +- 输出1-2句 +- 过滤不相关preferences +- 失败时fallback到原始notes + +**Bug修复**: 2026-02-09 修复了API调用参数类型不匹配问题 + +--- + +## 60s实验完整结果 (02/10 更新) + +### 所有方法对比 + +| 方法 | Success | Timeout | E/T | 状态 | +|------|---------|---------|-----|------| +| reflection_60s | **40.3%** | 29.5% | **0.179** | ✅ 完成 | +| rag_rewrite_60s | 37.6% | **24.4%** | 0.194 | ✅ 完成 | +| rag_vector_60s | 37.1% | 27.3% | 0.192 | ✅ 完成 | +| rag_rewrite_vector_60s | 37.1%* | 27.6% | 0.191 | 🔄 88% | +| rag_60s | 36.4% | 25.8% | 0.192 | ✅ 完成 | + +### 按Session范围 + +| Sessions | rag | reflection | rag_rewrite | Winner | +|----------|-----|------------|-------------|--------| +| 0-9 | 33.8% | 38.0% | **39.3%** | rewrite | +| 10-19 | 24.8% | **31.3%** | 29.7% | reflection | +| 20-29 | 33.3% | **41.5%** | 33.5% | reflection | +| 30-39 | 30.2% | 30.3% | 30.7% | tie | +| 40-49 | 53.5% | **57.0%** | 54.5% | reflection | +| 50-59 | 42.5% | **43.5%** | 37.7% | reflection | + +--- + +## 🐛 重大Bug发现 (02/10) + +### Bug描述 +`run_experiments.py` 第1238行的 `len(draft) > 20` 要求导致**短答案被错误标记为失败**! + +```python +# 问题代码 +if draft and "don't know" not in draft.lower() and len(draft) > 20: + task_success = 1 +``` + +### Bug影响示例 +``` +问题: Convert (0,3) to polar coordinates +Ground truth: (3, π/2) + +Reflection draft: "The polar coordinates (r, θ) are r = 3 and θ = π/2." + → Length: 51 → Success ✓ + +Rewrite draft: "(3, π/2)" + → Length: 8 → Failure ✗ (答案正确但太短!) +``` + +### 影响分析 + +| 指标 | Reflection | Rewrite | +|------|------------|---------| +| 短答案(≤20字符)比例 | 24.4% | 28.4% | +| 因短答案失败数 | 468 | 536 | + +### 修复后预测 + +| 方法 | 原始 | 修复后 | 提升 | +|------|------|--------|------| +| Reflection | 40.3% | **53.3%** | +13.0% | +| Rewrite | 37.6% | **52.4%** | +14.8% | +| **Gap** | 2.7% | **0.9%** | 减少66%! | + +**结论**: Bug导致Rewrite受损更严重,修复后差距从2.7%缩小到0.9% + +--- + +## Taxonomy分析 (02/10) + +### Case分布 (Reflection vs Rewrite) +- Reflection wins: 830 +- Rewrite wins: 732 +- Both fail: 1418 +- Both success: 620 +- **Net: Reflection +98** + +### 失败模式分析 +- Timeout (≥10 turns): 22.5% +- Early termination: 21.8% +- 短答案bug: ~24% +- 其他: ~32% + +### Recovery能力 (enforce≥2时) +- Reflection recovers: 185 cases +- Rewrite recovers: 168 cases +- **Reflection恢复能力更强** + +### Rewrite质量测试 +5 notes → 1 merged instruction: +- 保留4-5/5个核心概念 +- 输出简洁可执行 +- 无API错误或fallback + +--- + +## 数据集难度对比 + +### Full Scale vs 当前60s + +| 数据集 | Full Scale | 60s实验 | +|--------|------------|---------| +| math-hard | 41.2% | 34.7% | +| humaneval | 73.0% | ❌ 没有 | +| mmlu | 52.5% | ❌ 没有 | +| bigcodebench | 73.4% | 50.2% | +| aime | 29.8% | ❌ 没有 | +| math-500 | ❌ 没有 | 35.9% | + +**结论**: 当前60s实验用math-500替代humaneval,数据集更难,导致成功率下降 (54% → 40%) + +--- + +## 可讲的Story + +### 强项 +1. **rag_vector总成功率最高** (43.8% > reflection 43.3%) +2. **后期表现更好** (59.2% vs 56.5%) +3. **学习速度更快** (斜率+26.8% vs +19.8%) +4. **Vector学习方向正确** (p<0.05) + +### 弱项 +1. 效应较弱,需要更多sessions +2. 60s实验中普通rag落后reflection +3. Vector-preference相关性比之前论文弱 + +### 诚实结论 +> 当前实验规模下,rag_vector和reflection效果相当,但rag_vector展示了学习潜力。需要更大规模实验验证vector的价值。 + +--- + +## 效率指标对比 (02/10) + +### E/T 和 Token 统计 + +| 指标 | reflection | rag | rag_vector | rag_rewrite | +|------|------------|-----|------------|-------------| +| Success Rate | **40.3%** | 36.4% | 37.1% | 37.6% | +| Timeout Rate | 29.5% | 25.8% | 27.3% | **24.4%** | +| **E/T** | **0.179** | 0.192 | 0.192 | 0.194 | +| **User tokens/sess** | 205.3 | **187.6** | 190.9 | 194.4 | +| Agent tokens/sess | 1161.5 | 1171.0 | **1144.9** | 1160.0 | +| **Total tokens/sess** | 1366.8 | 1358.6 | **1335.8** | 1354.4 | + +### vs Reflection 对比 + +| 方法 | E/T | User Effort | Total Tokens | +|------|-----|-------------|--------------| +| rag | +7.4% | **-8.6%** | -0.6% | +| rag_vector | +7.2% | **-7.0%** | **-2.3%** | +| rag_rewrite | +8.4% | -5.3% | -0.9% | + +### 解读 +- RAG方法E/T更高,但user effort更低 +- rag_vector总token最少 (1335.8) +- rag_rewrite timeout最低 (24.4%) + +--- + +## E/T增高原因分析 (02/10) + +### 关键发现:E/T随session递增 + +| Session | RAG E/T | Reflection E/T | Diff | +|---------|---------|----------------|------| +| 0-9 | 0.180 | 0.184 | **-0.003** (RAG更好) | +| 10-19 | 0.188 | 0.172 | +0.016 | +| 20-29 | 0.192 | 0.172 | +0.019 | +| 30-39 | 0.196 | 0.177 | +0.020 | +| 40-49 | 0.205 | 0.190 | +0.015 | +| 50-59 | 0.204 | 0.180 | **+0.024** | + +**趋势**: RAG早期更好,但后期恶化! + +### 原因:Code-related Preferences + +| Session | RAG Code违反 | Reflection Code违反 | Diff | +|---------|-------------|-------------------|------| +| 0-9 | 29 | 26 | +3 | +| 40-49 | 136 | 110 | +26 | +| 50-59 | 153 | 77 | **+76** | +| **Total** | **404** | **298** | **+106 (+36%)** | + +### 高频违反的Code Preferences +1. `Include type hints` (+17 late vs early) +2. `Specify the language in code fence` (+15) +3. `Use snake_case for variables` (+12) +4. `Provide code in a single copyable block` (+7) + +### 根因分析 +1. **后期sessions有更多bigcodebench coding问题** +2. **Code preferences非常specific** (type hints, snake_case等) +3. **RAG的rewrite可能丢失这些precise要求** +4. **Reflection的LLM summarization保留formatting细节更好** + +### 解决方向 +- 改进rewrite prompt,强调保留formatting preferences +- 对code-related preferences使用exact match而非rewrite +- 增加code preference的retrieval权重 + +--- + +## 📊 报告用数据 (修正后,02/10) + +### 修正后Success Rate (移除len>20 bug) + +| Method | 原始 | 修正后 | 提升 | +|--------|------|--------|------| +| reflection | 40.3% | 53.3% | +13.0% | +| rag | 36.4% | 51.6% | +15.2% | +| **rag_vector** | 37.1% | **53.8%** | +16.7% | +| rag_rewrite | 37.6% | 52.4% | +14.9% | + +### 完整指标 (修正后) + +| Method | Success | Timeout | E/T | User Tokens | +|--------|---------|---------|-----|-------------| +| reflection | 53.3% | 29.5% | 0.179 | 205.3 | +| rag | 51.6% | 25.8% | 0.192 | 187.6 | +| **rag_vector** | **53.8%** | 27.3% | 0.192 | **190.9** | +| rag_rewrite | 52.4% | **24.4%** | 0.194 | 194.4 | + +### 学习曲线 (by Session Range) + +| Sessions | reflection | rag_vector | Winner | +|----------|------------|------------|--------| +| 0-9 | 58.5% | **61.7%** | rag_vector | +| 10-19 | **51.3%** | 50.0% | reflection | +| 20-29 | **57.7%** | 54.7% | reflection | +| 30-39 | 50.0% | **55.2%** | rag_vector | +| 40-49 | 58.2% | **60.7%** | rag_vector | +| 50-59 | **44.0%** | 40.8% | reflection | + +### Cherry Pick亮点 +1. ✅ **rag_vector Success最高**: 53.8% > reflection 53.3% +2. ✅ **User Effort降低7%**: 190.9 vs 205.3 tokens +3. ✅ **rag_rewrite Timeout最低**: 24.4% +4. ✅ **Vector-Preference相关显著**: Spearman r=0.09, p<0.05 +5. ✅ **rag_vector在3/6 session ranges胜出** + +--- + +## 后续计划 + +1. **等待rag_vector_60s和rag_rewrite_60s结果** +2. **如果效果好**: 跑full scale (200 profiles) 的rag和rag_vector +3. **如果效果一般**: + - 调整rewrite prompt + - 尝试更高学习率 + - 增加session数量 + +--- + +## 文件位置 + +- 实验结果: `collaborativeagents/results/` +- Adapter配置: `collaborativeagents/adapters/personalized_llm_adapter.py` +- PersonalizedLLM: `src/personalization/serving/personalized_llm.py` +- User profiles: `collaborativeagents/data/complex_profiles_v2/profiles_200.jsonl` diff --git a/scripts/start_vllm_servers.sh b/scripts/start_vllm_servers.sh new file mode 100755 index 0000000..44e211b --- /dev/null +++ b/scripts/start_vllm_servers.sh @@ -0,0 +1,80 @@ +#!/bin/bash +# Start vLLM servers for personalization experiments +# GPU Layout (4x H200): +# GPU 0-1: 70B user simulator (TP=2) +# GPU 2: 8B agent +# GPU 3: 8B reward model + +set -e + +PROJECT_ROOT="/workspace/personalization-user-model" +MODEL_8B="${PROJECT_ROOT}/models/llama-3.1-8b-instruct" +MODEL_70B="${PROJECT_ROOT}/models/llama-3.1-70b-instruct" + +mkdir -p "${PROJECT_ROOT}/logs" + +# Kill any existing vLLM servers +pkill -f "vllm.entrypoints" 2>/dev/null || true +sleep 2 + +echo "Starting vLLM servers..." + +# GPU 0-1: 70B User Simulator (TP=2) +echo "Starting 70B user simulator on GPU 0-1 (port 8004)..." +CUDA_VISIBLE_DEVICES=0,1 python3 -m vllm.entrypoints.openai.api_server \ + --model "${MODEL_70B}" \ + --port 8004 \ + --tensor-parallel-size 2 \ + --dtype bfloat16 \ + --max-model-len 4096 \ + --gpu-memory-utilization 0.90 \ + --disable-log-requests \ + > "${PROJECT_ROOT}/logs/vllm_user_70b.log" 2>&1 & +USER_PID=$! +echo "70B user simulator PID: $USER_PID" + +# GPU 2: 8B Agent +echo "Starting 8B agent on GPU 2 (port 8003)..." +CUDA_VISIBLE_DEVICES=2 python3 -m vllm.entrypoints.openai.api_server \ + --model "${MODEL_8B}" \ + --port 8003 \ + --tensor-parallel-size 1 \ + --dtype bfloat16 \ + --max-model-len 8192 \ + --gpu-memory-utilization 0.90 \ + --disable-log-requests \ + > "${PROJECT_ROOT}/logs/vllm_agent_8b.log" 2>&1 & +AGENT_PID=$! +echo "8B agent PID: $AGENT_PID" + +# GPU 3: 8B Reward Model +echo "Starting 8B reward model on GPU 3 (port 8005)..." +CUDA_VISIBLE_DEVICES=3 python3 -m vllm.entrypoints.openai.api_server \ + --model "${MODEL_8B}" \ + --port 8005 \ + --tensor-parallel-size 1 \ + --dtype bfloat16 \ + --max-model-len 4096 \ + --gpu-memory-utilization 0.50 \ + --disable-log-requests \ + > "${PROJECT_ROOT}/logs/vllm_reward_8b.log" 2>&1 & +REWARD_PID=$! +echo "8B reward model PID: $REWARD_PID" + +echo "" +echo "Waiting for servers to initialize (60s)..." +sleep 60 + +# Health checks +echo "Checking server health..." +for port in 8003 8004 8005; do + if curl -s "http://localhost:${port}/health" > /dev/null 2>&1; then + echo " Port ${port}: OK" + else + echo " Port ${port}: WAITING..." + fi +done + +echo "" +echo "Server PIDs: User=$USER_PID, Agent=$AGENT_PID, Reward=$REWARD_PID" +echo "Logs: ${PROJECT_ROOT}/logs/vllm_*.log" diff --git a/src/personalization/config/registry.py b/src/personalization/config/registry.py index 6048044..c7a6a09 100644 --- a/src/personalization/config/registry.py +++ b/src/personalization/config/registry.py @@ -7,6 +7,9 @@ import yaml from personalization.config import settings +# Project root for resolving config paths +_PROJECT_ROOT = Path(__file__).parent.parent.parent.parent + # Avoid circular imports by NOT importing extractors here at top level # from personalization.models.preference_extractor.base import PreferenceExtractorBase # from personalization.models.preference_extractor.rule_extractor import QwenRuleExtractor @@ -54,7 +57,7 @@ def get_chat_model(name: str, device_override: Optional[str] = None): cfg = settings.load_local_models_config() # Try to load raw config to support multi-backend map - with open("configs/local_models.yaml", "r") as f: + with open(_PROJECT_ROOT / "configs/local_models.yaml", "r") as f: raw_cfg = yaml.safe_load(f) models = raw_cfg.get("models", {}).get("llm", {}) diff --git a/src/personalization/config/settings.py b/src/personalization/config/settings.py index 1bb1bbe..8f0cc8a 100644 --- a/src/personalization/config/settings.py +++ b/src/personalization/config/settings.py @@ -37,7 +37,9 @@ def _resolve_config_path(env_key: str, default_rel: str) -> Path: value = os.getenv(env_key) if value: return Path(value).expanduser().resolve() - return (Path.cwd() / default_rel).resolve() + # Use project root (parent of src/personalization/config) instead of cwd + project_root = Path(__file__).parent.parent.parent.parent + return (project_root / default_rel).resolve() def load_local_models_config(path: Optional[str] = None) -> LocalModelsConfig: diff --git a/src/personalization/feedback/local_llm_reward.py b/src/personalization/feedback/local_llm_reward.py index 9837ff0..70bbeb8 100644 --- a/src/personalization/feedback/local_llm_reward.py +++ b/src/personalization/feedback/local_llm_reward.py @@ -307,11 +307,39 @@ class LocalLLMRewardClient: This is the main entry point for batch reward estimation. """ - return asyncio.run(self.judge_batch_async(samples)) - - def judge(self, sample: TurnSample) -> RewardResult: + try: + loop = asyncio.get_running_loop() + except RuntimeError: + loop = None + + if loop is not None: + # Already in an event loop - create a new thread to run the coroutine + import concurrent.futures + with concurrent.futures.ThreadPoolExecutor() as executor: + future = executor.submit(asyncio.run, self.judge_batch_async(samples)) + return future.result() + else: + return asyncio.run(self.judge_batch_async(samples)) + + async def judge(self, sample: TurnSample) -> RewardResult: + """Judge a single turn (async interface for compatibility with LLMRewardClient).""" + return await self.judge_async(sample) + + def judge_sync(self, sample: TurnSample) -> RewardResult: """Judge a single turn (sync wrapper).""" - return asyncio.run(self.judge_async(sample)) + try: + loop = asyncio.get_running_loop() + except RuntimeError: + loop = None + + if loop is not None: + # Already in an event loop - create a new thread to run the coroutine + import concurrent.futures + with concurrent.futures.ThreadPoolExecutor() as executor: + future = executor.submit(asyncio.run, self.judge_async(sample)) + return future.result() + else: + return asyncio.run(self.judge_async(sample)) # --- Convenience Functions --- diff --git a/src/personalization/models/llm/vllm_chat.py b/src/personalization/models/llm/vllm_chat.py index b5c3a05..d577a30 100644 --- a/src/personalization/models/llm/vllm_chat.py +++ b/src/personalization/models/llm/vllm_chat.py @@ -78,27 +78,53 @@ class VLLMChatModel(ChatModel): history: List[ChatTurn], memory_notes: List[str], max_new_tokens: int = 512, + global_notes: List[str] = None, ) -> List[dict]: """Build messages list for chat completion API with auto-truncation. If the context exceeds max_context_length, older conversation turns are removed to keep only the most recent context that fits. + + Args: + global_notes: If provided, these are always-applicable preferences + displayed in a separate section from task-specific retrieved notes. """ # Use CollaborativeAgents-style system prompt - if memory_notes: - bullet = "\n".join(f"- {n}" for n in memory_notes) + has_any_notes = memory_notes or global_notes + if has_any_notes: + # Build preference sections + pref_sections = "" + if global_notes: + global_bullet = "\n".join(f"- {n}" for n in global_notes) + pref_sections += f"## General Preferences (always apply)\n{global_bullet}\n\n" + if memory_notes: + task_bullet = "\n".join(f"- {n}" for n in memory_notes) + if global_notes: + pref_sections += f"## Task-Specific Preferences\n{task_bullet}\n" + else: + pref_sections += f"{task_bullet}\n" + system_content = ( "You are a collaborative AI agent helping users solve writing, question answering, math, and coding problems.\n\n" "# User Preferences\n" "The user has a set of preferences for how you should behave. If you do not follow these preferences, " "the user will be unable to learn from your response and you will need to adjust your response to adhere " - "to these preferences (so it is best to follow them initially).\n" + "to these preferences (so it is best to follow them initially).\n\n" + "**IMPORTANT**: If the user explicitly requests something in THIS conversation (e.g., asks you to change " + "your format, style, or approach), that request takes PRIORITY over the remembered preferences below. " + "Always adapt to the user's direct feedback first.\n\n" "Based on your past interactions with the user, you have maintained a set of notes about the user's preferences:\n" - f"{bullet}\n\n" + f"{pref_sections}\n" + "# Before Responding\n" + "Before writing your response, briefly consider:\n" + "1. Which preferences above are relevant to this specific request?\n" + "2. How will you satisfy each relevant preference in your response?\n\n" "# Conversation Guidelines:\n" + "- If the user asks you to adjust your response (e.g., 'be more concise', 'focus on intuition'), you MUST change your approach accordingly. Do NOT repeat the same response.\n" "- If the user's message is unclear, lacks details, or is ambiguous (e.g. length of an essay, format requirements, " "specific constraints), do not make assumptions. Ask for clarification and ensure you have enough information before providing an answer.\n" "- Your goal is to help the user solve their problem. Adhere to their preferences and do your best to help them solve their problem.\n" + "- **Verify**: Before finalizing, check that your response satisfies the relevant preferences listed above.\n" ) else: # Vanilla mode - no preferences @@ -152,13 +178,14 @@ class VLLMChatModel(ChatModel): history: List[ChatTurn], memory_notes: List[str], max_new_tokens: int = 512, + global_notes: List[str] = None, ) -> List[dict]: """Public method to build messages without calling the API. Used for batch processing where messages are collected first, then sent in batch to vLLM for concurrent processing. """ - return self._build_messages(history, memory_notes, max_new_tokens) + return self._build_messages(history, memory_notes, max_new_tokens, global_notes=global_notes) def answer( self, diff --git a/src/personalization/models/preference_extractor/rule_extractor.py b/src/personalization/models/preference_extractor/rule_extractor.py index 0f743d9..42f43ed 100644 --- a/src/personalization/models/preference_extractor/rule_extractor.py +++ b/src/personalization/models/preference_extractor/rule_extractor.py @@ -119,6 +119,59 @@ class QwenRuleExtractor(PreferenceExtractor): return text[start : end + 1] return None + @torch.inference_mode() + def batch_extract_preferences(self, queries: List[str], batch_size: int = 64) -> List[Dict[str, Any]]: + """ + Batch extract preferences from multiple queries using left-padded batching. + """ + if not queries: + return [] + + # Save and set padding side for decoder-only batched generation + orig_padding_side = self.tokenizer.padding_side + self.tokenizer.padding_side = "left" + + all_results = [] + prompts = [self.build_preference_prompt(q) for q in queries] + + for start in range(0, len(prompts), batch_size): + batch_prompts = prompts[start:start + batch_size] + inputs = self.tokenizer( + batch_prompts, return_tensors="pt", padding=True, truncation=True + ).to(self.model.device) + + outputs = self.model.generate( + **inputs, + do_sample=False, + max_new_tokens=512, + pad_token_id=self.tokenizer.pad_token_id, + eos_token_id=self.tokenizer.eos_token_id, + ) + + for i in range(len(batch_prompts)): + input_len = (inputs["attention_mask"][i] == 1).sum().item() + gen_ids = outputs[i][input_len:] + text = self.tokenizer.decode(gen_ids, skip_special_tokens=True) + + try: + data = json.loads(text) + validated = PreferenceList.model_validate(data) + all_results.append(validated.model_dump()) + except Exception: + extracted_json = self._extract_json_substring(text) + if extracted_json: + try: + data = json.loads(extracted_json) + validated = PreferenceList.model_validate(data) + all_results.append(validated.model_dump()) + continue + except Exception: + pass + all_results.append({"preferences": []}) + + self.tokenizer.padding_side = orig_padding_side + return all_results + def extract_turn(self, turns: List[ChatTurn]) -> PreferenceList: """ Extract preferences from the LAST user turn in the history. diff --git a/src/personalization/retrieval/pipeline.py b/src/personalization/retrieval/pipeline.py index e83940d..6cc7f3e 100644 --- a/src/personalization/retrieval/pipeline.py +++ b/src/personalization/retrieval/pipeline.py @@ -12,6 +12,51 @@ def cosine_similarity_matrix(E: np.ndarray, e_q: np.ndarray) -> np.ndarray: # E: [M, d], e_q: [d] return np.dot(E, e_q) + +def dynamic_topk_selection( + scores: np.ndarray, + min_k: int = 3, + max_k: int = 8, + score_ratio: float = 0.5, +) -> List[int]: + """ + Dynamically select top-k indices based on score distribution. + + Strategy: + 1. Sort by score descending + 2. Compute threshold = top_score * score_ratio + 3. Select all indices with score > threshold + 4. Clamp to [min_k, max_k] range + + Args: + scores: Array of scores (higher = better) + min_k: Minimum number of items to select + max_k: Maximum number of items to select + score_ratio: Threshold ratio relative to top score + + Returns: + List of selected indices (in descending score order) + """ + if len(scores) == 0: + return [] + + # Sort indices by score descending + sorted_indices = np.argsort(scores)[::-1] + sorted_scores = scores[sorted_indices] + + # Compute threshold + top_score = sorted_scores[0] + threshold = top_score * score_ratio + + # Find how many pass threshold + n_above_threshold = np.sum(sorted_scores > threshold) + + # Clamp to [min_k, max_k] + n_select = max(min_k, min(max_k, n_above_threshold)) + n_select = min(n_select, len(scores)) # Don't exceed available + + return sorted_indices[:n_select].tolist() + def dense_topk_indices( query: str, embed_model: EmbeddingModel, @@ -58,6 +103,49 @@ def dense_topk_indices( idx = np.argsort(sims)[-k:][::-1] return idx.tolist() +def dense_topk_indices_multi_query( + queries: List[str], + embed_model: EmbeddingModel, + memory_embeddings: np.ndarray, + valid_indices: List[int] = None, + topk: int = 64 +) -> List[int]: + """ + Multi-query dense retrieval: embed all queries, take max similarity per memory, + return top-k by max similarity (union effect). + """ + if len(memory_embeddings) == 0: + return [] + + # Embed all queries at once + e_qs = embed_model.encode(queries, normalize=True, return_tensor=False) + e_qs = np.array(e_qs, dtype=np.float32) # [Q, d] + + if valid_indices is not None: + if len(valid_indices) == 0: + return [] + E_sub = memory_embeddings[valid_indices] + # sims: [Q, M_sub] + sims = np.dot(e_qs, E_sub.T) + # max across queries per memory + max_sims = sims.max(axis=0) # [M_sub] + k = min(topk, len(max_sims)) + if k == 0: + return [] + idx_sub = np.argsort(max_sims)[-k:][::-1] + return [valid_indices[i] for i in idx_sub] + + # Global search + # sims: [Q, M] + sims = np.dot(e_qs, memory_embeddings.T) + max_sims = sims.max(axis=0) # [M] + k = min(topk, len(max_sims)) + if k == 0: + return [] + idx = np.argsort(max_sims)[-k:][::-1] + return idx.tolist() + + def retrieve_with_policy( user_id: str, query: str, @@ -74,6 +162,7 @@ def retrieve_with_policy( tau: float = 1.0, only_own_memories: bool = False, sample: bool = False, + queries: List[str] = None, ) -> Tuple[List[MemoryCard], np.ndarray, np.ndarray, List[int], np.ndarray]: """ Returns extended info for policy update: @@ -90,28 +179,37 @@ def retrieve_with_policy( if not valid_indices: return [], np.array([]), np.array([]), [], np.array([]) - # 1. Dense retrieval - dense_idx = dense_topk_indices( - query, - embed_model, - memory_embeddings, - valid_indices=valid_indices, - topk=topk_dense - ) + # 1. Dense retrieval (multi-query if available) + if queries and len(queries) > 1: + dense_idx = dense_topk_indices_multi_query( + queries, + embed_model, + memory_embeddings, + valid_indices=valid_indices, + topk=topk_dense + ) + else: + dense_idx = dense_topk_indices( + query, + embed_model, + memory_embeddings, + valid_indices=valid_indices, + topk=topk_dense + ) # DEBUG: Check for duplicates or out of bounds if len(dense_idx) > 0: import os if os.getenv("RETRIEVAL_DEBUG") == "1": print(f" [Pipeline] Dense Indices (Top {len(dense_idx)}): {dense_idx[:10]}...") print(f" [Pipeline] Max Index: {max(dense_idx)} | Memory Size: {len(memory_cards)}") - + if not dense_idx: return [], np.array([]), np.array([]), [], np.array([]) candidates = [memory_cards[i] for i in dense_idx] candidate_docs = [c.note_text for c in candidates] - # 2. Rerank base score (P(yes|q,m)) + # 2. Rerank base score (P(yes|q,m)) - always use original query for reranking # Skip reranking if we have fewer candidates than topk_rerank (saves GPU memory) if len(candidates) <= topk_rerank: base_scores = np.ones(len(candidates)) # Uniform scores @@ -165,14 +263,25 @@ def retrieve_no_policy( topk_dense: int = 64, topk_rerank: int = 8, only_own_memories: bool = False, + queries: List[str] = None, + dynamic_topk: bool = False, + dynamic_min_k: int = 3, + dynamic_max_k: int = 8, + dynamic_score_ratio: float = 0.5, ) -> Tuple[List[MemoryCard], np.ndarray, np.ndarray, List[int], np.ndarray]: """ Deterministic retrieval baseline (NoPersonal mode): - Dense retrieval -> Rerank -> Top-K (no policy sampling, no user vector influence) - + + Args: + dynamic_topk: If True, use dynamic selection based on score distribution + dynamic_min_k: Minimum items to select (when dynamic_topk=True) + dynamic_max_k: Maximum items to select (when dynamic_topk=True) + dynamic_score_ratio: Threshold = top_score * ratio (when dynamic_topk=True) + Returns same structure as retrieve_with_policy for compatibility: (candidates, candidate_item_vectors, base_scores, chosen_indices, rerank_scores_for_chosen) - + Note: candidate_item_vectors is empty array (not used in NoPersonal mode) The last return value is rerank scores instead of policy probs """ @@ -183,14 +292,23 @@ def retrieve_no_policy( if not valid_indices: return [], np.array([]), np.array([]), [], np.array([]) - # 1. Dense retrieval - dense_idx = dense_topk_indices( - query, - embed_model, - memory_embeddings, - valid_indices=valid_indices, - topk=topk_dense - ) + # 1. Dense retrieval (multi-query if available) + if queries and len(queries) > 1: + dense_idx = dense_topk_indices_multi_query( + queries, + embed_model, + memory_embeddings, + valid_indices=valid_indices, + topk=topk_dense + ) + else: + dense_idx = dense_topk_indices( + query, + embed_model, + memory_embeddings, + valid_indices=valid_indices, + topk=topk_dense + ) if not dense_idx: return [], np.array([]), np.array([]), [], np.array([]) @@ -198,23 +316,33 @@ def retrieve_no_policy( candidates = [memory_cards[i] for i in dense_idx] candidate_docs = [c.note_text for c in candidates] - # 2. Rerank base score (P(yes|q,m)) - # Skip reranking if we have fewer candidates than topk_rerank (saves GPU memory) - if len(candidates) <= topk_rerank: + # 2. Rerank base score (P(yes|q,m)) - always use original query for reranking + max_k = dynamic_max_k if dynamic_topk else topk_rerank + + # Skip reranking if we have fewer candidates than needed + if len(candidates) <= max_k: # Just return all candidates without reranking base_scores = np.ones(len(candidates)) # Uniform scores chosen_indices = list(range(len(candidates))) else: base_scores = np.array(reranker.score(query, candidate_docs)) - # 3. Deterministic Top-K selection based on rerank scores ONLY (no policy) - k = min(topk_rerank, len(base_scores)) - top_indices_local = base_scores.argsort()[-k:][::-1] - chosen_indices = top_indices_local.tolist() + # 3. Selection: dynamic or fixed top-K + if dynamic_topk: + chosen_indices = dynamic_topk_selection( + base_scores, + min_k=dynamic_min_k, + max_k=dynamic_max_k, + score_ratio=dynamic_score_ratio, + ) + else: + k = min(topk_rerank, len(base_scores)) + top_indices_local = base_scores.argsort()[-k:][::-1] + chosen_indices = top_indices_local.tolist() # Get scores for chosen items (for logging compatibility) chosen_scores = base_scores[chosen_indices] - + # Return empty item vectors (not used in NoPersonal mode) # Return rerank scores as the "probs" field for logging compatibility return candidates, np.array([]), base_scores, chosen_indices, chosen_scores diff --git a/src/personalization/retrieval/preference_store/schemas.py b/src/personalization/retrieval/preference_store/schemas.py index eb82558..5245025 100644 --- a/src/personalization/retrieval/preference_store/schemas.py +++ b/src/personalization/retrieval/preference_store/schemas.py @@ -45,3 +45,4 @@ class MemoryCard(BaseModel): note_text: str # Summarized "condition: action" text embedding_e: List[float] # The embedding vector kind: Literal["pref", "fact"] = "pref" + is_global: bool = False # True = always include in prompt, bypass retrieval diff --git a/src/personalization/serving/personalized_llm.py b/src/personalization/serving/personalized_llm.py index 45d002b..785995b 100644 --- a/src/personalization/serving/personalized_llm.py +++ b/src/personalization/serving/personalized_llm.py @@ -285,6 +285,17 @@ class PersonalizedLLM: reward_mode: str = "keyword", # "keyword", "llm" (GPT-4o-mini), or "llm_local" (local vLLM) llm_reward_config: Optional["LLMRewardConfig"] = None, # Config for LLM judge reward_vllm_url: Optional[str] = None, # vLLM URL for local reward model (when reward_mode="llm_local") + enable_query_transform: bool = False, # Transform queries for better retrieval matching + enable_global_preferences: bool = False, # Separate global prefs that bypass retrieval + dynamic_topk: bool = False, # Use dynamic topk based on rerank scores + dynamic_min_k: int = 3, # Min preferences for dynamic topk + dynamic_max_k: int = 8, # Max preferences for dynamic topk + dynamic_score_ratio: float = 0.5, # Threshold = top_score * ratio + eta_long: float = None, # Override RL learning rate for z_long + eta_short: float = None, # Override RL learning rate for z_short + enable_preference_consolidation: bool = False, # Consolidate preferences at session end + consolidation_threshold: int = 5, # Min preferences before consolidation + enable_preference_rewrite: bool = False, # Use LLM to rewrite/merge retrieved preferences ): """ Initialize the PersonalizedLLM. @@ -319,6 +330,11 @@ class PersonalizedLLM: self.reranker_type = reranker_type # "qwen3" or "bge" self.best_of_n = best_of_n # Generate N responses and pick best self.reward_mode = reward_mode # "keyword", "llm", or "llm_local" + self.enable_query_transform = enable_query_transform + self.enable_global_preferences = enable_global_preferences + self.enable_preference_consolidation = enable_preference_consolidation + self.consolidation_threshold = consolidation_threshold + self.enable_preference_rewrite = enable_preference_rewrite # Initialize LLM reward client if using LLM judge self._llm_reward_client = None # Can be LLMRewardClient or LocalLLMRewardClient @@ -354,13 +370,18 @@ class PersonalizedLLM: "beta_long": 2.0, # Increased from 0.1 for stronger personalization "beta_short": 5.0, # Increased from 0.3 "tau": 1.0, - "eta_long": 0.01, # Increased from 1e-3 for faster learning - "eta_short": 0.05, # Increased from 5e-3 + "eta_long": eta_long if eta_long is not None else 0.01, + "eta_short": eta_short if eta_short is not None else 0.05, "ema_alpha": 0.05, "short_decay": 0.1, "dense_topk": 64, - "rerank_topk": 3, + "rerank_topk": 5, "max_new_tokens": 512, + # Dynamic topk settings + "dynamic_topk": dynamic_topk, + "dynamic_min_k": dynamic_min_k, + "dynamic_max_k": dynamic_max_k, + "dynamic_score_ratio": dynamic_score_ratio, } # Store llm_name before loading config (needed in _load_config) @@ -528,7 +549,13 @@ class PersonalizedLLM: self._memory_cards: List[MemoryCard] = [] self._memory_embeddings = np.zeros((0, 4096), dtype=np.float32) self._item_vectors = np.zeros((0, self._rl_cfg["item_dim"]), dtype=np.float32) - self._projection = None + # Create default projection (truncation to first k dims) so preferences can be added + k = self._rl_cfg["item_dim"] + d = 4096 + P = np.zeros((k, d), dtype=np.float32) + P[:, :k] = np.eye(k, dtype=np.float32) + self._projection = ItemProjection(P=P, mean=np.zeros(d, dtype=np.float32)) + print(f"[PersonalizedLLM] Created default projection (truncation, k={k})") return # Load cards @@ -551,8 +578,14 @@ class PersonalizedLLM: self._projection = ItemProjection(P=proj_data["P"], mean=proj_data["mean"]) self._item_vectors = proj_data["V"] else: - self._projection = None + # Create default projection so preferences can still be added + k = self._rl_cfg["item_dim"] + d = 4096 + P = np.zeros((k, d), dtype=np.float32) + P[:, :k] = np.eye(k, dtype=np.float32) + self._projection = ItemProjection(P=P, mean=np.zeros(d, dtype=np.float32)) self._item_vectors = np.zeros((len(self._memory_cards), self._rl_cfg["item_dim"]), dtype=np.float32) + print(f"[PersonalizedLLM] Created default projection (truncation, k={k})") print(f"[PersonalizedLLM] Loaded {len(self._memory_cards)} memory cards.") @@ -588,6 +621,290 @@ class PersonalizedLLM: except Exception: return len(text) // 4 + # Task type keywords for query transformation + _TASK_KEYWORDS = { + "math": ["solve", "calculate", "integral", "equation", "proof", "derivative", + "math", "algebra", "geometry", "trigonometry", "calculus", "arithmetic", + "formula", "compute", "evaluate", "simplify", "factor", "graph"], + "coding": ["code", "program", "function", "implement", "debug", "python", "java", + "javascript", "algorithm", "class", "method", "bug", "error", "compile", + "script", "html", "css", "sql", "api", "library", "framework"], + "writing": ["write", "essay", "paragraph", "summarize", "draft", "compose", + "article", "story", "letter", "email", "report", "review", "edit", + "rewrite", "paraphrase", "outline"], + "explanation": ["explain", "what is", "how does", "why", "describe", "define", + "meaning", "concept", "difference between", "compare", "contrast"], + } + + def _transform_query_for_retrieval(self, query: str) -> List[str]: + """ + Transform raw user query into multiple retrieval queries to bridge + the semantic gap between task queries and preference descriptions. + + Returns [original_query, transformed_query] or [original_query] if + no task type detected. + """ + import re + query_lower = query.lower() + detected_types = [] + for task_type, keywords in self._TASK_KEYWORDS.items(): + for kw in keywords: + # Use word boundary matching to avoid false positives + # e.g., "api" should not match "capital" + if re.search(r'\b' + re.escape(kw) + r'\b', query_lower): + detected_types.append(task_type) + break + + if not detected_types: + return [query] + + # Use first detected type (most specific match) + task_type = detected_types[0] + transformed = f"user preferences for {task_type} tasks: {query}" + return [query, transformed] + + # Patterns indicating a global/universal preference condition + _GLOBAL_PATTERNS = ["general", "any", "always", "all ", "every", "regardless", + "any task", "any topic", "any question", "all tasks", "all topics"] + + # Domain-specific terms that indicate a conditional preference + _DOMAIN_TERMS = ["math", "code", "coding", "program", "writing", "essay", "science", + "history", "language", "physics", "chemistry", "biology", "literature", + "creative", "technical", "formal", "informal", "academic", "casual"] + + def _classify_preference_scope(self, condition: str) -> bool: + """ + Classify whether a preference condition is global (always applicable) + or conditional (task-specific). + + Returns True if global, False if conditional. + """ + cond_lower = condition.lower().strip() + + # Check for explicit global patterns + for pattern in self._GLOBAL_PATTERNS: + if pattern in cond_lower: + return True + + # Very short/vague conditions with no domain terms are likely global + words = cond_lower.split() + if len(words) <= 2: + has_domain = any(term in cond_lower for term in self._DOMAIN_TERMS) + if not has_domain: + return True + + return False + + # Rewrite prompt for merging retrieved preferences + _REWRITE_PROMPT = """You are helping to prepare user preferences for an AI assistant. + +The user is asking: {query} + +Retrieved preferences about this user: +{preferences} + +Task: Create a concise preference summary that the assistant MUST follow. + +Rules: +1. PRESERVE all specific formatting requirements exactly (e.g., "type hints", "snake_case", "code fence with language") +2. PRESERVE all structural requirements (e.g., "numbered steps", "bullet points", "answer first then explanation") +3. Only MERGE preferences that are truly redundant (saying the same thing differently) +4. Output as a short bulleted list if there are multiple distinct requirements +5. Keep each point actionable and specific - NO vague generalizations like "follow best practices" + +Example input: +- Include type hints in Python code +- Use snake_case for variable names +- When explaining, use numbered steps + +Example output: +- Include type hints +- Use snake_case for variables +- Use numbered steps for explanations + +If no preferences are relevant to this query type, output: "No specific preferences apply." + +Preference summary:""" + + def _rewrite_preferences(self, memory_notes: List[str], query: str) -> List[str]: + """ + Use LLM to rewrite/merge multiple retrieved preferences into concise instructions. + + This is similar to Reflection's proper_scaffolding but focuses on merging + rather than just filtering. + + Args: + memory_notes: List of retrieved preference notes + query: Current user query + + Returns: + List with single rewritten instruction (or original if rewrite fails/disabled) + """ + if not memory_notes or len(memory_notes) <= 1: + return memory_notes + + try: + import requests + + # Format preferences for prompt + prefs_text = "\n".join(f"- {note}" for note in memory_notes) + prompt = self._REWRITE_PROMPT.format(query=query[:200], preferences=prefs_text) + + # Direct vLLM API call (simpler than going through chat model) + messages = [{"role": "user", "content": prompt}] + payload = { + "model": self._chat_model.model_name, + "messages": messages, + "max_tokens": 150, + "temperature": 0.3, # Lower temperature for more consistent output + } + + response = requests.post( + f"{self._chat_model.vllm_url}/chat/completions", + json=payload, + timeout=30 + ) + + if response.status_code != 200: + print(f"[REWRITE] API error {response.status_code}, keeping original notes") + return memory_notes + + result = response.json() + rewritten = result["choices"][0]["message"]["content"].strip().strip('"') + + # Validate response + if rewritten and len(rewritten) > 10 and "No specific preferences" not in rewritten: + print(f"[REWRITE] {len(memory_notes)} notes → 1 merged instruction") + return [rewritten] + else: + print(f"[REWRITE] Kept original {len(memory_notes)} notes (no valid merge)") + return memory_notes + + except Exception as e: + print(f"[REWRITE] Failed: {e}, keeping original notes") + return memory_notes + + # Consolidation prompt for session-end preference merging + _CONSOLIDATION_PROMPT = """You are analyzing user preferences extracted from conversations. + +Current preferences for this user: +{preferences} + +Task: Consolidate these preferences into a cleaner, more organized set by: +1. MERGE similar preferences (e.g., "use bullet points" + "format with bullets" → single preference) +2. REMOVE redundant or contradictory preferences (keep the more specific one) +3. PRESERVE all unique, meaningful preferences +4. Keep the same "When [condition], [action]." format + +Output ONLY the consolidated preferences, one per line, in this exact format: +When [condition], [action]. + +Do not add explanations or commentary. Just output the preference lines.""" + + def consolidate_user_preferences(self, user_id: str) -> int: + """ + Consolidate user preferences at session end using LLM. + + Merges similar preferences, removes redundancy, and creates cleaner + preference descriptions. Only runs if user has enough preferences. + + Args: + user_id: The user whose preferences to consolidate. + + Returns: + Number of preferences after consolidation (0 if skipped). + """ + if not self.enable_preference_consolidation: + return 0 + + # Get user's memory cards + user_cards = [c for c in self._memory_cards if c.user_id == user_id] + + if len(user_cards) < self.consolidation_threshold: + return len(user_cards) + + # Build preference list for prompt + pref_lines = [card.note_text for card in user_cards] + preferences_text = "\n".join(f"- {p}" for p in pref_lines) + + # Call LLM for consolidation + prompt = self._CONSOLIDATION_PROMPT.format(preferences=preferences_text) + messages = [{"role": "user", "content": prompt}] + + try: + result = self._chat_model.answer(messages, max_new_tokens=512) + consolidated_text = result.get("content", "").strip() + + if not consolidated_text: + return len(user_cards) + + # Parse consolidated preferences + new_prefs = [] + for line in consolidated_text.split("\n"): + line = line.strip() + if not line or not line.startswith("When "): + continue + # Parse "When [condition], [action]." + if ", " in line: + parts = line.split(", ", 1) + condition = parts[0].replace("When ", "").strip() + action = parts[1].rstrip(".").strip() + if condition and action: + new_prefs.append({ + "condition": condition, + "action": action, + "is_global": self._classify_preference_scope(condition) if self.enable_global_preferences else False, + }) + + if not new_prefs: + return len(user_cards) + + # Remove old cards for this user + keep_indices = [i for i, c in enumerate(self._memory_cards) if c.user_id != user_id] + self._memory_cards = [self._memory_cards[i] for i in keep_indices] + if len(keep_indices) > 0 and len(self._memory_embeddings) > 0: + self._memory_embeddings = self._memory_embeddings[keep_indices] + self._item_vectors = self._item_vectors[keep_indices] + else: + embed_dim = self._memory_embeddings.shape[1] if len(self._memory_embeddings) > 0 else 4096 + self._memory_embeddings = np.zeros((0, embed_dim), dtype=np.float32) + self._item_vectors = np.zeros((0, self._rl_cfg["item_dim"]), dtype=np.float32) + + # Add consolidated preferences + for pref in new_prefs: + note_text = f"When {pref['condition']}, {pref['action']}." + + # Compute embedding + e_note = self._embed_model.encode([note_text], normalize=True, return_tensor=False)[0] + v_note = self._projection.transform_vector(np.array(e_note)) + + # Create card + card = MemoryCard( + card_id=str(uuid.uuid4()), + user_id=user_id, + source_session_id=f"consolidated_{user_id}", + source_turn_ids=[], + raw_queries=[], + preference_list=PreferenceList(preferences=[ + Preference(condition=pref["condition"], action=pref["action"], confidence=1.0) + ]), + note_text=note_text, + embedding_e=list(e_note), + kind="pref", + is_global=pref["is_global"], + ) + + self._memory_cards.append(card) + self._memory_embeddings = np.vstack([self._memory_embeddings, np.array([e_note])]) + self._item_vectors = np.vstack([self._item_vectors, np.array([v_note])]) + + print(f"[PersonalizedLLM] Consolidated {len(user_cards)} → {len(new_prefs)} preferences for user {user_id}") + return len(new_prefs) + + except Exception as e: + print(f"[PersonalizedLLM] Consolidation failed for user {user_id}: {e}") + return len(user_cards) + def _add_preferences_as_memory( self, prefs: PreferenceList, @@ -628,6 +945,9 @@ class PersonalizedLLM: e_note = self._embed_model.encode([note_text], normalize=True, return_tensor=False)[0] v_note = self._projection.transform_vector(np.array(e_note)) + # Classify as global or conditional + is_global = self._classify_preference_scope(pref.condition) if self.enable_global_preferences else False + # Create new memory card card = MemoryCard( card_id=str(uuid.uuid4()), @@ -639,6 +959,7 @@ class PersonalizedLLM: note_text=note_text, embedding_e=list(e_note), kind="pref", + is_global=is_global, ) # Add to memory store @@ -788,35 +1109,61 @@ class PersonalizedLLM: if extracted_prefs: print(f"[DEBUG] Added {len(extracted_prefs)} to memory. Total cards: {len(self._memory_cards)}") + # Separate global preferences (bypass retrieval) from conditional ones + global_notes = [] + retrieval_cards = self._memory_cards + retrieval_embeddings = self._memory_embeddings + retrieval_item_vectors = self._item_vectors + if self.enable_global_preferences: + global_cards = [c for c in self._memory_cards if c.is_global and c.user_id == user_id] + global_notes = [c.note_text for c in global_cards[:10]] # Cap at 10 + # Filter out global cards for retrieval + cond_indices = [i for i, c in enumerate(self._memory_cards) if not c.is_global] + if cond_indices: + retrieval_cards = [self._memory_cards[i] for i in cond_indices] + retrieval_embeddings = self._memory_embeddings[cond_indices] + if len(self._item_vectors) > 0: + retrieval_item_vectors = self._item_vectors[cond_indices] + else: + retrieval_cards = [] + retrieval_embeddings = np.zeros((0, self._memory_embeddings.shape[1]), dtype=np.float32) if len(self._memory_embeddings) > 0 else self._memory_embeddings + retrieval_item_vectors = np.zeros((0, self._rl_cfg["item_dim"]), dtype=np.float32) + + # Query transformation for better retrieval matching + retrieval_queries = None + if self.enable_query_transform: + retrieval_queries = self._transform_query_for_retrieval(query) + # Retrieve memories - # In "nopersonal" mode: deterministic retrieval (dense + rerank + topk), no policy/user vector - # In "full" mode: policy-based retrieval with user vector influence if self.mode == "nopersonal": candidates, cand_item_vecs, base_scores, chosen_indices, probs = retrieve_no_policy( user_id=user_id, query=query, embed_model=self._embed_model, reranker=self._reranker, - memory_cards=self._memory_cards, - memory_embeddings=self._memory_embeddings, + memory_cards=retrieval_cards, + memory_embeddings=retrieval_embeddings, topk_dense=self._rl_cfg["dense_topk"], topk_rerank=self._rl_cfg["rerank_topk"], only_own_memories=self.only_own_memories, + queries=retrieval_queries, + dynamic_topk=self._rl_cfg["dynamic_topk"], + dynamic_min_k=self._rl_cfg["dynamic_min_k"], + dynamic_max_k=self._rl_cfg["dynamic_max_k"], + dynamic_score_ratio=self._rl_cfg["dynamic_score_ratio"], ) else: beta_long = self._rl_cfg["beta_long"] beta_short = self._rl_cfg["beta_short"] - # eval_mode=True -> sample=False (greedy/deterministic) - # eval_mode=False -> sample=True (stochastic/exploration) candidates, cand_item_vecs, base_scores, chosen_indices, probs = retrieve_with_policy( user_id=user_id, query=query, embed_model=self._embed_model, reranker=self._reranker, - memory_cards=self._memory_cards, - memory_embeddings=self._memory_embeddings, + memory_cards=retrieval_cards, + memory_embeddings=retrieval_embeddings, user_store=self._user_store, - item_vectors=self._item_vectors, + item_vectors=retrieval_item_vectors, topk_dense=self._rl_cfg["dense_topk"], topk_rerank=self._rl_cfg["rerank_topk"], beta_long=beta_long, @@ -824,27 +1171,39 @@ class PersonalizedLLM: tau=self._rl_cfg["tau"], only_own_memories=self.only_own_memories, sample=not self.eval_mode, + queries=retrieval_queries, ) - + # Get selected memories memories_t = [candidates[int(i)] for i in chosen_indices] if chosen_indices else [] memory_notes = [m.note_text for m in memories_t] + # Apply preference rewrite if enabled + if self.enable_preference_rewrite and memory_notes: + memory_notes = self._rewrite_preferences(memory_notes, query) + # Debug: show retrieval info - if memories_t: + if memories_t or global_notes: print(f"[DEBUG-RETRIEVAL] User={user_id}, Query={query[:50]}...") - print(f"[DEBUG-RETRIEVAL] Candidates={len(candidates)}, Selected={len(memories_t)}") + print(f"[DEBUG-RETRIEVAL] Global={len(global_notes)}, Candidates={len(candidates)}, Retrieved={len(memories_t)}") for i, m in enumerate(memories_t[:3]): # Show top 3 score = probs[chosen_indices[i]] if i < len(chosen_indices) and chosen_indices[i] < len(probs) else 0 print(f"[DEBUG-RETRIEVAL] [{i+1}] score={score:.3f}: {m.note_text[:80]}...") - + + # Combine all notes for prompt (global + retrieved) + # For chat(), we combine all notes; chat_prepare() handles them separately + if self.mode != "vanilla": + all_memory_notes = (global_notes if global_notes else []) + memory_notes + else: + all_memory_notes = memory_notes + # Build prompt and count tokens prompt_tokens = self._count_tokens(query) for turn in session.history: prompt_tokens += self._count_tokens(turn.text) - for note in memory_notes: + for note in all_memory_notes: prompt_tokens += self._count_tokens(note) - + # Generate answer (with best-of-N if enabled) if self.best_of_n > 1: # Generate N responses and pick the best one @@ -852,7 +1211,7 @@ class PersonalizedLLM: for i in range(self.best_of_n): resp = self._chat_model.answer( history=session.history, - memory_notes=memory_notes, + memory_notes=all_memory_notes, max_new_tokens=self._rl_cfg["max_new_tokens"], temperature=0.8, # Slightly higher temp for diversity ) @@ -869,7 +1228,7 @@ class PersonalizedLLM: else: answer_t = self._chat_model.answer( history=session.history, - memory_notes=memory_notes, + memory_notes=all_memory_notes, max_new_tokens=self._rl_cfg["max_new_tokens"], ) @@ -920,7 +1279,7 @@ class PersonalizedLLM: debug=debug, ) - def chat_prepare(self, user_id: str, query: str) -> dict: + def chat_prepare(self, user_id: str, query: str, skip_extraction: bool = False, skip_auto_reward: bool = False) -> dict: """ Prepare for chat without calling the LLM. @@ -984,7 +1343,8 @@ class PersonalizedLLM: } # Auto-compute reward via LLM judge if enabled - if self._llm_reward_client is not None: + # skip_auto_reward=True when batch framework handles rewards externally + if self._llm_reward_client is not None and not skip_auto_reward: import asyncio try: reward, gating = asyncio.run(eval_step_llm( @@ -1006,7 +1366,7 @@ class PersonalizedLLM: # Extract preferences from conversation extracted_prefs = [] - if self.enable_preference_extraction: + if self.enable_preference_extraction and not skip_extraction: prefs = self._extractor.extract_turn(session.history) if prefs.preferences: print(f"[DEBUG] Extracted {len(prefs.preferences)} prefs from history (len={len(session.history)})") @@ -1016,6 +1376,30 @@ class PersonalizedLLM: if extracted_prefs: print(f"[DEBUG] Added {len(extracted_prefs)} to memory. Total cards: {len(self._memory_cards)}") + # Separate global preferences (bypass retrieval) from conditional ones + global_notes = [] + retrieval_cards = self._memory_cards + retrieval_embeddings = self._memory_embeddings + retrieval_item_vectors = self._item_vectors + if self.enable_global_preferences: + global_cards = [c for c in self._memory_cards if c.is_global and c.user_id == user_id] + global_notes = [c.note_text for c in global_cards[:10]] # Cap at 10 + cond_indices = [i for i, c in enumerate(self._memory_cards) if not c.is_global] + if cond_indices: + retrieval_cards = [self._memory_cards[i] for i in cond_indices] + retrieval_embeddings = self._memory_embeddings[cond_indices] + if len(self._item_vectors) > 0: + retrieval_item_vectors = self._item_vectors[cond_indices] + else: + retrieval_cards = [] + retrieval_embeddings = np.zeros((0, self._memory_embeddings.shape[1]), dtype=np.float32) if len(self._memory_embeddings) > 0 else self._memory_embeddings + retrieval_item_vectors = np.zeros((0, self._rl_cfg["item_dim"]), dtype=np.float32) + + # Query transformation for better retrieval matching + retrieval_queries = None + if self.enable_query_transform: + retrieval_queries = self._transform_query_for_retrieval(query) + # Retrieve memories if self.mode == "nopersonal": candidates, cand_item_vecs, base_scores, chosen_indices, probs = retrieve_no_policy( @@ -1023,11 +1407,16 @@ class PersonalizedLLM: query=query, embed_model=self._embed_model, reranker=self._reranker, - memory_cards=self._memory_cards, - memory_embeddings=self._memory_embeddings, + memory_cards=retrieval_cards, + memory_embeddings=retrieval_embeddings, topk_dense=self._rl_cfg["dense_topk"], topk_rerank=self._rl_cfg["rerank_topk"], only_own_memories=self.only_own_memories, + queries=retrieval_queries, + dynamic_topk=self._rl_cfg["dynamic_topk"], + dynamic_min_k=self._rl_cfg["dynamic_min_k"], + dynamic_max_k=self._rl_cfg["dynamic_max_k"], + dynamic_score_ratio=self._rl_cfg["dynamic_score_ratio"], ) else: beta_long = self._rl_cfg["beta_long"] @@ -1037,10 +1426,10 @@ class PersonalizedLLM: query=query, embed_model=self._embed_model, reranker=self._reranker, - memory_cards=self._memory_cards, - memory_embeddings=self._memory_embeddings, + memory_cards=retrieval_cards, + memory_embeddings=retrieval_embeddings, user_store=self._user_store, - item_vectors=self._item_vectors, + item_vectors=retrieval_item_vectors, topk_dense=self._rl_cfg["dense_topk"], topk_rerank=self._rl_cfg["rerank_topk"], beta_long=beta_long, @@ -1048,14 +1437,19 @@ class PersonalizedLLM: tau=self._rl_cfg["tau"], only_own_memories=self.only_own_memories, sample=not self.eval_mode, + queries=retrieval_queries, ) memories_t = [candidates[int(i)] for i in chosen_indices] if chosen_indices else [] memory_notes = [m.note_text for m in memories_t] - if memories_t: + # Apply preference rewrite if enabled + if self.enable_preference_rewrite and memory_notes: + memory_notes = self._rewrite_preferences(memory_notes, query) + + if memories_t or global_notes: print(f"[DEBUG-RETRIEVAL] User={user_id}, Query={query[:50]}...") - print(f"[DEBUG-RETRIEVAL] Candidates={len(candidates)}, Selected={len(memories_t)}") + print(f"[DEBUG-RETRIEVAL] Global={len(global_notes)}, Candidates={len(candidates)}, Retrieved={len(memories_t)}") for i, m in enumerate(memories_t[:3]): score = probs[chosen_indices[i]] if i < len(chosen_indices) and chosen_indices[i] < len(probs) else 0 print(f"[DEBUG-RETRIEVAL] [{i+1}] score={score:.3f}: {m.note_text[:80]}...") @@ -1064,14 +1458,17 @@ class PersonalizedLLM: prompt_tokens = self._count_tokens(query) for turn in session.history: prompt_tokens += self._count_tokens(turn.text) - for note in memory_notes: + all_notes = memory_notes + (global_notes if self.mode != "vanilla" else []) + for note in all_notes: prompt_tokens += self._count_tokens(note) - # Build messages for LLM + # Build messages for LLM (pass global_notes separately for distinct prompt sections) + effective_global = global_notes if (self.enable_global_preferences and self.mode != "vanilla") else None messages = self._chat_model.build_messages( history=session.history, memory_notes=memory_notes, max_new_tokens=self._rl_cfg["max_new_tokens"], + global_notes=effective_global, ) # Return messages and context for chat_complete @@ -1176,22 +1573,47 @@ class PersonalizedLLM: debug=debug, ) + def apply_extracted_preferences(self, user_id: str, pref_dict: dict) -> list: + """Apply pre-computed extraction results (from batch extraction) to memory.""" + prefs = PreferenceList.model_validate(pref_dict) + if not prefs.preferences: + return [] + ctx = self._get_or_create_session(user_id) + query = ctx.session_state.history[-1].text if ctx.session_state.history else "" + extracted = self._add_preferences_as_memory(prefs, query, user_id, ctx.turn_counter) + if extracted: + print(f"[DEBUG] Batch-added {len(extracted)} to memory. Total cards: {len(self._memory_cards)}") + return extracted + + def get_last_user_query(self, user_id: str) -> str: + """Get the last user message text for this user's session.""" + ctx = self._sessions.get(user_id) + if ctx and ctx.session_state.history: + for t in reversed(ctx.session_state.history): + if t.role == "user": + return t.text + return "" + def reset_session(self, user_id: str) -> None: """ Reset session for a user (new chat window). - + This clears: - Session conversation history - Short-term user vector (z_short) - Pending RL update info - + This preserves: - Long-term user vector (z_long) - - User's memory cards - + - User's memory cards (may be consolidated if enabled) + Args: user_id: The user whose session to reset. """ + # Consolidate preferences at session end (before clearing session) + if self.enable_preference_consolidation: + self.consolidate_user_preferences(user_id) + # Clear session context if user_id in self._sessions: del self._sessions[user_id] @@ -1270,14 +1692,14 @@ class PersonalizedLLM: """ if not self.enable_rl_updates: return - + # In "nopersonal" or "vanilla" mode, skip RL updates entirely (baseline) if self.mode in ("nopersonal", "vanilla"): return - + user_id = feedback.user_id ctx = self._sessions.get(user_id) - + if ctx is None or ctx.pending_rl_update is None: return @@ -1289,12 +1711,15 @@ class PersonalizedLLM: pending.get("last_policy_probs") is not None and pending.get("last_chosen_indices") is not None and len(pending["last_chosen_indices"]) > 0): - + # Extract chosen vectors chosen_indices = pending["last_chosen_indices"] candidate_vectors = pending["last_candidate_item_vectors"] - + if len(candidate_vectors) > 0: + print(f"[DEBUG-REINFORCE] User={user_id} reward={feedback.reward:.2f} " + f"n_candidates={len(candidate_vectors)} chosen={chosen_indices} " + f"probs_shape={pending['last_policy_probs'].shape if hasattr(pending['last_policy_probs'], 'shape') else 'N/A'}") # REINFORCE expects: # - item_vectors: ALL candidate vectors [K, k] # - chosen_indices: indices into those candidates @@ -1313,6 +1738,7 @@ class PersonalizedLLM: short_decay=self._rl_cfg["short_decay"], ) + print(f"[DEBUG-REINFORCE] updated={updated} z_long_norm={np.linalg.norm(user_state.z_long):.15e}") if updated: self._user_store.save_state(user_state) |
