# TODO: Artifact Backend Integration (aidream / Python Server)

> **Status:** Block detection, model, parser, and stream processor integration complete. Post-stream persistence and testing needed.
> **Depends on:** Supabase RPC functions (already deployed in matrx-admin DB).

---

## What's Already Done

- [x] `"artifact"` added to `ATTRIBUTE_XML_BLOCKS` in `block_detector.py`
- [x] `extract_attribute_xml_block` handles artifact-specific metadata: `artifactId`, `artifactIndex`, `artifactType`, `artifactTitle`
- [x] `ARTIFACT = "artifact"` added to `BlockType` enum in `models/base.py`
- [x] `artifact` registered in `block_registry.py` with `MARKDOWN_COMPLETE` streaming behavior
- [x] `models/artifact.py` — Pydantic model: `ArtifactBlockData`
- [x] `parsers/artifact_parser.py` — `parse_artifact()` extracts structured data from metadata
- [x] `stream_processor.py` — artifact in `_MARKDOWN_COMPLETE_TYPES`, parser map, metadata forwarding

---

## What Needs to Be Done

### 1. Add Artifact to Test Content

**File:** `tests/test_content.md`

Add test cases at the end of the canonical test corpus:

```markdown
Here is a dashboard I built for you:

<artifact id="artifact_1" type="iframe" title="Revenue Dashboard">
<html>
<body>
<h1>Revenue Dashboard</h1>
<p>Q1: $1.2M | Q2: $1.5M | Q3: $1.8M</p>
</body>
</html>
</artifact>

And here is some more analysis. Let me also create a code artifact:

<artifact id="artifact_2" type="code" title="Data Processing Script">
import pandas as pd

df = pd.read_csv('revenue.csv')
quarterly = df.groupby('quarter').sum()
print(quarterly)
</artifact>

That should help with your analysis.
```

This tests:
- Two artifacts in one message
- Different artifact types (`iframe` vs `code`)
- Prose interspersed between artifacts
- Artifact index extraction from `id="artifact_1"` and `id="artifact_2"`

---

### 2. Write Unit Tests

**File:** `tests/test_parsers/test_artifact_parser.py`

```python
from matrx_ai.processing.blocks.parsers.artifact_parser import parse_artifact


def test_parse_artifact_basic():
    result = parse_artifact(
        "<h1>Hello</h1>",
        metadata={
            "artifactId": "artifact_1",
            "artifactIndex": 1,
            "artifactType": "iframe",
            "artifactTitle": "Dashboard",
        },
    )
    assert result is not None
    assert result.artifact_id == "artifact_1"
    assert result.artifact_index == 1
    assert result.artifact_type == "iframe"
    assert result.title == "Dashboard"
    assert result.content == "<h1>Hello</h1>"


def test_parse_artifact_no_metadata():
    result = parse_artifact("<p>content</p>", metadata=None)
    assert result is not None
    assert result.artifact_id == "artifact-0"
    assert result.artifact_type == "text"


def test_parse_artifact_empty_content():
    result = parse_artifact("", metadata={"artifactId": "artifact_1"})
    assert result is None
```

**File:** `tests/test_block_detector.py` — add artifact detection tests:

```python
def test_detect_artifact_block():
    line = '<artifact id="artifact_1" type="iframe" title="Dashboard">'
    result = detect_attribute_xml_block(line)
    assert result is not None
    assert result["type"] == "artifact"
    assert result["attributes"]["id"] == "artifact_1"
    assert result["attributes"]["type"] == "iframe"
    assert result["attributes"]["title"] == "Dashboard"


def test_extract_artifact_block():
    lines = [
        '<artifact id="artifact_1" type="code" title="Script">',
        'print("hello")',
        '</artifact>',
    ]
    detection = detect_attribute_xml_block(lines[0])
    result = extract_attribute_xml_block(detection, 0, lines)
    assert result.content == 'print("hello")'
    assert result.metadata["isComplete"] is True
    assert result.metadata["artifactId"] == "artifact_1"
    assert result.metadata["artifactIndex"] == 1
    assert result.metadata["artifactType"] == "code"
```

---

### 3. Add End-to-End Stream Processor Test

**File:** `tests/test_stream_processor.py` — add artifact streaming test:

```python
def test_artifact_block_streaming():
    processor = StreamBlockProcessor()

    tokens = [
        "Here is the dashboard:\n\n",
        '<artifact id="artifact_1" type="iframe" title="Revenue">',
        "\n<h1>Revenue</h1>\n",
        "<p>Q1: $1.2M</p>\n",
        "</artifact>",
        "\n\nDone!",
    ]

    all_events = []
    for token in tokens:
        events = processor.process_token(token)
        all_events.extend(events)

    final_events = processor.finalize()
    all_events.extend(final_events)

    # Find the artifact block event
    artifact_events = [e for e in all_events if e.type == "artifact"]
    assert len(artifact_events) > 0

    final_artifact = artifact_events[-1]
    assert final_artifact.status.value == "complete"
    assert final_artifact.data is not None
    assert final_artifact.data["artifactId"] == "artifact_1"
    assert final_artifact.data["artifactType"] == "iframe"
    assert final_artifact.data["title"] == "Revenue"
```

---

### 4. Post-Stream Artifact Persistence (Optional — could be frontend responsibility)

If the Python server should handle persistence directly (instead of the frontend):

1. After stream finalization, collect all artifact block events
2. Call Supabase `cx_canvas_upsert` for each artifact:
   ```python
   await supabase.rpc('cx_canvas_upsert', {
       'p_user_id': user_id,
       'p_message_id': message_id,
       'p_artifact_index': artifact_data['artifactIndex'],
       'p_type': artifact_data['artifactType'],
       'p_title': artifact_data['title'],
       'p_content': {
           'data': artifact_content,
           'type': artifact_data['artifactType'],
           'metadata': {}
       },
       'p_source_type': 'model_direct'
   })
   ```

**Decision needed:** Should persistence happen in the Python server or in the Next.js frontend? Either works. The RPC functions are idempotent (upsert behavior), so calling from both sides won't cause issues — but one canonical location is cleaner.

---

### 5. Verify Integrity Check Passes

Run the stream processor once and check the startup integrity output:

```python
processor = StreamBlockProcessor()
# Integrity check runs automatically on first use
# Should see NO red errors related to "artifact"
```

The artifact block should pass all integrity checks because:
- It's in `_MARKDOWN_COMPLETE_TYPES` (classified)
- It has a parser in `_build_parser_map()` (Gap 3b check passes)

---

## File Reference

| File | Purpose |
|------|---------|
| `block_detector.py` | Detects `<artifact>` as attribute-bearing XML |
| `models/base.py` | `BlockType.ARTIFACT` enum value |
| `models/artifact.py` | `ArtifactBlockData` Pydantic model |
| `parsers/artifact_parser.py` | `parse_artifact()` — structured data extraction |
| `block_registry.py` | Registered with `MARKDOWN_COMPLETE` behavior |
| `stream_processor.py` | Processes artifact blocks in streaming pipeline |
| `tag_detection_buffer.py` | Prevents `<artif` flicker during streaming (automatic) |
| `tests/test_content.md` | Canonical test corpus — needs artifact examples |

---

## Known Subtlety

The `tag_detection_buffer.py` already handles partial XML tags like `<artif` during streaming. Since `"artifact"` starts with `<a` which is a common HTML tag start, the buffer's suppression window is important here — it will hold back tokens until it can determine whether `<a` is the start of `<artifact` or just an `<a href=...>` link. The current buffer logic handles this correctly because it waits for the full tag name before releasing.
