Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion app/services/llm.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import json
import os
from string import Template

import requests
from requests.exceptions import RequestException, Timeout
Expand Down Expand Up @@ -28,7 +29,10 @@ def build_prompt(self, current_field: str, current_type: str = "string"):
with open(prompt_path, "r") as f:
template = f.read()

return template.format(field=current_field, type=current_type, text=self._transcript_text)
# Dollar placeholders leave literal braces in JSON examples untouched.
return Template(template).safe_substitute(
field=current_field, type=current_type, text=self._transcript_text
)

def main_loop(self):
timeout = 45
Expand Down
6 changes: 3 additions & 3 deletions app/services/prompt.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ only a single string containing the identified value for the JSON field.
If you don't identify the value in the provided text, return "-1".
---
DATA:
Target JSON field to find in text: {field}
Expected value type: {type}
Target JSON field to find in text: $field
Expected value type: $type

TEXT: {text}
TEXT: $text
9 changes: 9 additions & 0 deletions docs/2. PROJECT_STRUCTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,15 @@ app/
Tests live in `tests/`, run with `make test` (pytest inside the app container). `conftest.py` holds shared fixtures. Name files `test_<area>.py` and mirror what you're testing: API endpoint tests alongside `test_api.py`, model/DB tests alongside `test_model.py`.
Note: Tests will be undergoing many changes hence the docs can be outdated, Please raise issue if tests or docs are outdated.

### Editing the extraction prompt

`app/services/prompt.txt` uses `string.Template` placeholders: `$field`, `$type`,
and `$text`. Literal braces, including JSON examples, do not need escaping.
Unknown placeholders are left unchanged. Use `$$` to include a literal dollar
sign before a known placeholder name (for example, `$$field` renders as `$field`).
Substituted field names and transcripts are inserted as-is, without further
placeholder expansion.

## Docker & scripts

- `docker/dev/` - development image (hot reload, source mounted) and its compose file. This is what `make up` runs.
Expand Down
55 changes: 55 additions & 0 deletions tests/test_llm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""Regression tests for prompt rendering without calling Ollama."""

from unittest.mock import mock_open, patch

import pytest

from app.services.llm import LLM


def test_build_prompt_uses_bundled_template():
prompt = LLM(transcript_text="John Smith signed the form.").build_prompt(
"Signature", "signature"
)

assert "Target JSON field to find in text: Signature" in prompt
assert "Expected value type: signature" in prompt
assert "TEXT: John Smith signed the form." in prompt


@pytest.mark.parametrize(
"literal",
[
'EXAMPLE: {"value": "John Smith"}',
'EXAMPLE: {"value": {"names": ["John", "Smith"]}}',
"An unmatched opening brace: {",
"An unmatched closing brace: }",
"Unknown slots: {unknown} $unknown ${unknown}",
"Literal dollars: $5 and a trailing $",
],
)
def test_build_prompt_preserves_literal_template_content(literal):
template = "$field | $type | $text\n" + literal
with patch("app.services.llm.open", mock_open(read_data=template)):
prompt = LLM(transcript_text="John Smith").build_prompt(
"Signature", "signature"
)

assert prompt == "Signature | signature | John Smith\n" + literal


def test_build_prompt_does_not_substitute_inside_values():
field = "Name $type ${text} {field}"
field_type = "string $text {type}"
transcript = 'Received {"value": "$field"}, ${type}, and {text}.'
with patch("app.services.llm.open", mock_open(read_data="$field | $type | $text")):
prompt = LLM(transcript_text=transcript).build_prompt(field, field_type)

assert prompt == f"{field} | {field_type} | {transcript}"


def test_build_prompt_defaults_to_string_type():
with patch("app.services.llm.open", mock_open(read_data="$field | $type | $text")):
prompt = LLM(transcript_text="John Smith").build_prompt("Name")

assert prompt == "Name | string | John Smith"
Loading