diff --git a/CHANGELOG.yaml b/CHANGELOG.yaml index 0d1dfbe6..749020ea 100644 --- a/CHANGELOG.yaml +++ b/CHANGELOG.yaml @@ -26,6 +26,7 @@ - "Pi Bridge Client: The Docker image now ships with pi preinstalled, and pi's configuration directory (models.json, auth.json) is mounted from ./pi next to the compose file, so the client works in Docker out of the box." - "Scene Browser Landing Page: The Quick Load recent-scene cards are smaller and scale with the viewport width, so a full row of recents no longer pushes the Scene Library far down the page." fixes: + - "Prompt Templates: The `system_time` template function crashed with 'Invalid format string' on Windows — it used strftime codes only available on Linux/macOS. The time is now formatted platform-independently with identical output." - "Director Chat: Images created through the director chat's image generation action never appeared in the chat — the generation completed and the image was saved to the scene's assets, but the 'Image Generated' message was inserted into a newly created orphan chat instead of the conversation that requested it. The message is now inserted into the initiating chat." - "Node Editor: The Build Prompt node's `memory_prompt` input socket and the Generate Response node's `action_type` input socket were silently ignored — only the node property ever took effect, so a graph computing either value dynamically and wiring it in had no effect. Non-empty wired values now win over the property (the property still applies when the socket is unconnected or resolves empty). Note: graphs that already had something connected to these sockets will start honoring the connection." - "Scene Library: Deleting the save file of the currently loaded scene is now refused with an error - previously the file (and its version history) was deleted but silently recreated by the next save or autosave, making the delete appear to not work. Load a different scene first." diff --git a/src/talemate/prompts/base.py b/src/talemate/prompts/base.py index baeaf25a..c3c28e51 100644 --- a/src/talemate/prompts/base.py +++ b/src/talemate/prompts/base.py @@ -16,7 +16,7 @@ import re import uuid from contextvars import ContextVar from datetime import datetime -from typing import Any +from typing import Any, Literal from enum import Enum import jinja2 @@ -1067,7 +1067,9 @@ class Prompt(pydantic.BaseModel): return "" return iso8601_diff_to_human(iso8601_time, scene.ts) - def system_time(self, format: str = "full") -> str: + def system_time( + self, format: Literal["full", "date", "time", "iso", "datetime"] = "full" + ) -> str: """ Returns the current system time in a clear, LLM-friendly format. @@ -1084,18 +1086,21 @@ class Prompt(pydantic.BaseModel): """ now = datetime.now() - if format == "full": - return now.strftime("%A, %B %-d, %Y at %-I:%M %p") - elif format == "date": - return now.strftime("%B %-d, %Y") + # %-d / %-I are glibc-only and raise ValueError on Windows, so the + # unpadded day and 12-hour clock are built from datetime attributes. + hour12 = now.hour % 12 or 12 + time_str = f"{hour12}:{now:%M} {now:%p}" + + if format == "date": + return f"{now:%B} {now.day}, {now.year}" elif format == "time": - return now.strftime("%-I:%M %p") + return time_str elif format == "iso": return now.strftime("%Y-%m-%dT%H:%M:%S") elif format == "datetime": return now.strftime("%Y-%m-%d %H:%M:%S") else: - return now.strftime("%A, %B %-d, %Y at %-I:%M %p") + return f"{now:%A}, {now:%B} {now.day}, {now.year} at {time_str}" def text_to_chunks(self, text: str, chunk_size: int = 512) -> list[str]: """ diff --git a/tests/prompts/test_base_extras.py b/tests/prompts/test_base_extras.py index a24234ff..feef3b1a 100644 --- a/tests/prompts/test_base_extras.py +++ b/tests/prompts/test_base_extras.py @@ -25,12 +25,15 @@ boundary, NOT Prompt.request). from __future__ import annotations +import locale from collections import deque +from datetime import datetime import pytest from conftest import client_responses +import talemate.prompts.base as prompts_base from talemate.prompts.base import ( JoinableList, Prompt, @@ -244,23 +247,62 @@ class TestRandomAndBullet: class TestSystemTime: - def test_full_format(self): - p = Prompt.from_text("X") - s = p.system_time("full") - # Should contain weekday name and 'at' - assert " at " in s + @pytest.fixture + def fixed_now(self, monkeypatch): + # The exact-string assertions below assume the C locale for %A/%B/%p. + prev_locale = locale.setlocale(locale.LC_TIME) + locale.setlocale(locale.LC_TIME, "C") - def test_iso_format(self): - p = Prompt.from_text("X") - s = p.system_time("iso") - # ISO-like: "YYYY-MM-DDTHH:MM:SS" - assert s[4] == "-" - assert "T" in s + def _freeze(dt): + class _FrozenDatetime(datetime): + @classmethod + def now(cls, tz=None): + return dt - def test_unknown_format_falls_back_to_full(self): + monkeypatch.setattr(prompts_base, "datetime", _FrozenDatetime) + + yield _freeze + locale.setlocale(locale.LC_TIME, prev_locale) + + def test_full_format(self, fixed_now): + fixed_now(datetime(2026, 2, 5, 14, 30, 45)) p = Prompt.from_text("X") - s = p.system_time("totally-bogus") - assert " at " in s + assert p.system_time("full") == "Thursday, February 5, 2026 at 2:30 PM" + + def test_date_format(self, fixed_now): + fixed_now(datetime(2026, 2, 5, 14, 30, 45)) + p = Prompt.from_text("X") + assert p.system_time("date") == "February 5, 2026" + + def test_time_format(self, fixed_now): + fixed_now(datetime(2026, 2, 5, 14, 30, 45)) + p = Prompt.from_text("X") + assert p.system_time("time") == "2:30 PM" + + def test_time_format_midnight(self, fixed_now): + fixed_now(datetime(2026, 2, 5, 0, 5, 0)) + p = Prompt.from_text("X") + assert p.system_time("time") == "12:05 AM" + + def test_time_format_noon(self, fixed_now): + fixed_now(datetime(2026, 2, 5, 12, 5, 0)) + p = Prompt.from_text("X") + assert p.system_time("time") == "12:05 PM" + + def test_iso_format(self, fixed_now): + fixed_now(datetime(2026, 2, 5, 14, 30, 45)) + p = Prompt.from_text("X") + assert p.system_time("iso") == "2026-02-05T14:30:45" + + def test_datetime_format(self, fixed_now): + fixed_now(datetime(2026, 2, 5, 14, 30, 45)) + p = Prompt.from_text("X") + assert p.system_time("datetime") == "2026-02-05 14:30:45" + + def test_unknown_format_falls_back_to_full(self, fixed_now): + fixed_now(datetime(2026, 2, 5, 14, 30, 45)) + p = Prompt.from_text("X") + assert p.system_time("totally-bogus") == "Thursday, February 5, 2026 at 2:30 PM" class TestTimeDiff: