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