import json from talemate.util.prompt import ( parse_response_section, extract_actions_block, clean_visible_response, ) # Helper to parse extracted content (since extract_actions_block now returns raw string) def parse_actions_content(content: str | None) -> list[dict] | None: """Parse the raw actions content string into a list of dicts.""" if not content: return None try: data = json.loads(content) if isinstance(data, dict): data = [data] if not isinstance(data, list): return None normalized = [] for item in data: if isinstance(item, list): for sub in item: if isinstance(sub, dict): name = sub.get("name") or sub.get("function") instructions = sub.get("instructions") or "" if name: normalized.append( {"name": str(name), "instructions": str(instructions)} ) continue if not isinstance(item, dict): continue name = item.get("name") or item.get("function") instructions = item.get("instructions") or "" if name: normalized.append( {"name": str(name), "instructions": str(instructions)} ) return normalized or None except json.JSONDecodeError: return None # ============================================================================ # Tests for parse_response_section # ============================================================================ class TestParseResponseSection: """Tests for the parse_response_section function.""" def test_basic_message_with_analysis(self): """Test extracting a MESSAGE after ANALYSIS block.""" response = """ This is some analysis text. This is the response message. """ result = parse_response_section(response) assert result == "This is the response message." def test_message_without_analysis(self): """Test extracting MESSAGE when no ANALYSIS block present.""" response = """ Simple message without analysis. """ result = parse_response_section(response) assert result == "Simple message without analysis." def test_actions_in_analysis_ignored(self): """Test that tags within ANALYSIS don't interfere.""" response = """ The best action would be test but we need to consider: - Multiple tags here - Even nested or malformed ones This is the actual message. """ result = parse_response_section(response) assert result == "This is the actual message." def test_nested_message_like_text_in_analysis(self): """Test MESSAGE-like text in ANALYSIS doesn't confuse parser.""" response = """ The user might want to see this but that's just analysis. We could also say "something else" in quotes. Real message here. """ result = parse_response_section(response) assert result == "Real message here." def test_decision_tag_in_analysis_ignored(self): """Test that tags within ANALYSIS don't interfere with MESSAGE parsing.""" response = """ The best decision would be option_a based on: - Multiple tags here - Even nested ones like option_b This is the actual message. """ result = parse_response_section(response) assert result == "This is the actual message." def test_decision_block_in_message_extracted(self): """Test that DECISION blocks within MESSAGE are extracted (but will be stripped later).""" response = """ Analysis text. Here is my response with a decision: The character should proceed cautiously. More message text after decision. """ result = parse_response_section(response) assert "Here is my response" in result assert "" in result # Note: The actual stripping happens in chat_clean_visible_response # This test just verifies the MESSAGE is extracted with DECISION intact # ============================================================================ # Tests for extract_actions_block # ============================================================================ class TestExtractActionsBlock: """Tests for the extract_actions_block function.""" def test_basic_actions_json(self): """Test extracting basic ACTIONS block with JSON.""" response = """ ```json [ {"name": "test_action", "instructions": "Do something"} ] ``` """ content = extract_actions_block(response) assert content is not None result = parse_actions_content(content) assert result is not None assert len(result) == 1 assert result[0]["name"] == "test_action" assert result[0]["instructions"] == "Do something" def test_actions_in_analysis_ignored(self): """Test that ACTIONS blocks within ANALYSIS are not extracted.""" response = """ We could perform ```json [{"name": "fake", "instructions": "This is just analysis"}] ``` but that's just analysis. Here's the real response. ```json [{"name": "real_action", "instructions": "This is the real action"}] ``` """ content = extract_actions_block(response) result = parse_actions_content(content) assert result is not None assert len(result) == 1 assert result[0]["name"] == "real_action" assert "real action" in result[0]["instructions"] def test_only_actions_in_analysis_returns_none(self): """ Test that if ACTIONS only appears within ANALYSIS (and not after), we return None since those are not real actions. """ response = """ We could use ```json [{"name": "fake_action", "instructions": "This is just theoretical"}] ``` but that's just analysis. Let me think about this more. """ result = extract_actions_block(response) assert result is None def test_actions_with_action_tag_in_analysis(self): """Test the edge case where tags appear in before .""" response = """ Looking at the scene, I notice several things: - The character could move to the door - Or they could speak to the other character - Maybe even hide behind something Based on this analysis, I recommend we proceed. I think the best course of action is to have them move cautiously. ```json [ {"name": "move", "instructions": "Move slowly toward the door"} ] ``` """ content = extract_actions_block(response) result = parse_actions_content(content) assert result is not None assert len(result) == 1 assert result[0]["name"] == "move" assert "door" in result[0]["instructions"] def test_decision_in_analysis_ignored(self): """Test that DECISION blocks within ANALYSIS are ignored when extracting ACTIONS.""" response = """ I need to decide on the approach cautious_approach. Also considering aggressive_approach as alternative. Based on my analysis, here's the plan. ```json [{"name": "proceed", "instructions": "Move forward"}] ``` """ content = extract_actions_block(response) result = parse_actions_content(content) assert result is not None assert len(result) == 1 assert result[0]["name"] == "proceed" def test_actions_and_decision_in_message(self): """Test that both ACTIONS and DECISION can appear in MESSAGE after ANALYSIS.""" response = """ Analyzing the scene with potential test and ```json [{"name": "fake", "instructions": "fake"}] ``` blocks. My decision: proceed_with_caution And here are the actions to take: ```json [{"name": "real_action", "instructions": "Do this"}] ``` """ content = extract_actions_block(response) result = parse_actions_content(content) assert result is not None assert len(result) == 1 assert result[0]["name"] == "real_action" # ============================================================================ # Tests for clean_visible_response # ============================================================================ class TestCleanVisibleResponse: """Tests for the clean_visible_response function.""" def test_removes_actions_block(self): """Test that ACTIONS blocks are removed.""" text = """Here is my response. ```json [{"name": "test"}] ``` More text after.""" result = clean_visible_response(text) assert result == "Here is my response.\n\nMore text after." def test_removes_decision_and_everything_after(self): """Test that everything from DECISION tag onwards is removed.""" text = """Here is my response. Choose option A This text should be removed too.""" result = clean_visible_response(text) assert result == "Here is my response." def test_removes_legacy_actions_block(self): """Test that legacy ```actions``` blocks are removed.""" text = """Here is my response. ```actions some action data ``` More text after.""" result = clean_visible_response(text) assert result == "Here is my response.\n\nMore text after." def test_removes_actions_then_decision(self): """Test removing both ACTIONS and DECISION blocks.""" text = """Here is my response. ```json [{"name": "test"}] ``` Everything from here onwards is removed""" result = clean_visible_response(text) assert result == "Here is my response." def test_no_special_tags(self): """Test text without special tags is unchanged.""" text = "Just a message with no decision or actions." result = clean_visible_response(text) assert result == "Just a message with no decision or actions." def test_case_insensitive(self): """Test that tag matching is case insensitive.""" text = """My response. ```json [{"name": "test"}] ``` removed""" result = clean_visible_response(text) assert result == "My response." def test_decision_without_closing_tag(self): """Test that DECISION without closing tag removes everything after.""" text = """Here is my response. This is my decision and all this text continues for many lines and should all be removed.""" result = clean_visible_response(text) assert result == "Here is my response." def test_multiple_actions_blocks(self): """Test that multiple ACTIONS blocks are all removed.""" text = """Start. ```json [{"name": "first"}] ``` Middle. ```json [{"name": "second"}] ``` End.""" result = clean_visible_response(text) assert "Start." in result assert "Middle." in result assert "End." in result assert "" not in result assert "first" not in result assert "second" not in result