Files
LLM-Robot-Control/tests/test_prompts.py
T

79 lines
4.1 KiB
Python

import asyncio
import json
from pathlib import Path
import tempfile
from types import SimpleNamespace
import unittest
from unittest.mock import AsyncMock, patch
import yaml
from assembly_dataset.config import Config, load_config
from assembly_dataset.pipeline import generate
from assembly_dataset.world import make_episode, training_rows
from assembly_dataset.language import action_text
class PromptTests(unittest.TestCase):
def test_custom_request_and_retry_reach_teacher(self):
from assembly_dataset.teacher import enrich, Plan
from llm.config import OpenAiConfig
config = Config(blocked_fraction=0)
config.teacher.fraction = 1.0
config.teacher.llm = OpenAiConfig(provider='openai', model='test')
config.prompts.teacher_user = 'REQUEST ${payload}'
config.prompts.teacher_retry = 'REPAIR ${payload}'
episode = make_episode(0, config)
agent = SimpleNamespace(run=AsyncMock(side_effect=[
SimpleNamespace(output=Plan(status='ready', actions=[])),
SimpleNamespace(output=Plan.model_validate({**episode['plan'], 'actions': [{**a, 'text': action_text(a, episode['scene'], config.prompts)} for a in episode['plan']['actions']]})),
]))
result, error = asyncio.run(enrich(episode, config.teacher, agent, config.prompts))
self.assertIsNone(error)
first, second = [call.args[0] for call in agent.run.call_args_list]
self.assertTrue(first.startswith('REQUEST {'))
self.assertTrue(second.startswith('REPAIR {'))
self.assertIn('validation_error', json.loads(second.removeprefix('REPAIR ')))
self.assertEqual(result['source']['provider'], 'openai')
def test_external_prompts_relative_paths_and_export(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
prompts = yaml.safe_load(Path('prompts.yaml').read_text())
prompts['training_system']['full'] = 'Custom training rules'
prompts['training_user'] = 'Input: ${payload}'
prompts['task_instructions']['mechanical'] = 'Custom assembly task'
(root / 'texts.yaml').write_text(yaml.safe_dump(prompts))
(root / 'config.yaml').write_text('prompts_file: texts.yaml\noutput_dir: data\ndomains: [mechanical]\n')
config = load_config(root / 'config.yaml')
episode = make_episode(0, config)
record = next(training_rows(episode, config))
self.assertEqual(episode['instruction'], 'Custom assembly task')
self.assertEqual(record['messages'][0]['content'], 'Custom training rules')
self.assertTrue(record['messages'][1]['content'].startswith('Input: {'))
self.assertEqual(config.output_dir, root / 'data')
def test_prompt_changes_prevent_mixed_resume(self):
with tempfile.TemporaryDirectory() as tmp:
config = Config(output_dir=Path(tmp), target_examples=1)
asyncio.run(generate(config))
config.prompts.teacher_system = 'Different rules'
with self.assertRaises(ValueError):
asyncio.run(generate(config))
def test_invalid_templates_are_rejected(self):
from assembly_dataset.prompts import Prompts
data = Config().prompts.model_dump()
for template in ('No scene', '${unknown}', '${payload.bad}'):
with self.subTest(template=template), self.assertRaises(ValueError):
Prompts.model_validate({**data, 'teacher_user': template})
def test_teacher_system_is_passed_to_agent(self):
from assembly_dataset.teacher import create_agent
from llm.config import OpenAiConfig
config = Config()
config.teacher.llm = OpenAiConfig(provider='openai', model='test')
config.prompts.teacher_system = 'Custom teacher instructions'
with patch('llm.factory.LLMModelProvider.create_from_config', new_callable=AsyncMock), patch('pydantic_ai.Agent') as agent:
asyncio.run(create_agent(config.teacher, config.prompts))
self.assertEqual(agent.call_args.kwargs['system_prompt'], 'Custom teacher instructions')