Skip to content
Merged
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
3 changes: 3 additions & 0 deletions echo/server/dembrane/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@
PROMPT_TEMPLATES_DIR = os.path.join(BASE_DIR, "prompt_templates")
logger.debug(f"PROMPT_TEMPLATES_DIR: {PROMPT_TEMPLATES_DIR}")

JSON_TEMPLATES_DIR = os.path.join(BASE_DIR, "json_templates")
logger.debug(f"JSON_TEMPLATES_DIR: {JSON_TEMPLATES_DIR}")

Comment on lines +82 to +84
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Solid drop-in, but double-check directory existence at boot

JSON_TEMPLATES_DIR is wired exactly like PROMPT_TEMPLATES_DIR—nice symmetry.
Tiny win: proactively os.makedirs(JSON_TEMPLATES_DIR, exist_ok=True) (or Path(...).mkdir) during startup so container images don’t bomb when the folder is missing.

+# Ensure the directory exists to avoid FileNotFoundError in CI / fresh containers
+os.makedirs(JSON_TEMPLATES_DIR, exist_ok=True)
🤖 Prompt for AI Agents
In echo/server/dembrane/config.py around lines 82 to 84, the JSON_TEMPLATES_DIR
is set but the directory existence is not ensured, which can cause runtime
errors if the directory is missing. Add a call to
os.makedirs(JSON_TEMPLATES_DIR, exist_ok=True) right after defining
JSON_TEMPLATES_DIR to create the directory if it does not exist, preventing
errors during container startup.

DIRECTUS_SECRET = os.environ.get("DIRECTUS_SECRET")
assert DIRECTUS_SECRET, "DIRECTUS_SECRET environment variable is not set"
logger.debug("DIRECTUS_SECRET: set")
Expand Down
69 changes: 65 additions & 4 deletions echo/server/dembrane/prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,20 @@
"""

import os
import json
import logging
from typing import Any
from typing import Any, Optional
from collections import defaultdict

from jinja2 import Environment, FileSystemLoader, select_autoescape

from dembrane.config import PROMPT_TEMPLATES_DIR
from dembrane.config import JSON_TEMPLATES_DIR, PROMPT_TEMPLATES_DIR

logger = logging.getLogger("prompts")

env = Environment(loader=FileSystemLoader(PROMPT_TEMPLATES_DIR), autoescape=select_autoescape())
prompt_env = Environment(
loader=FileSystemLoader(PROMPT_TEMPLATES_DIR), autoescape=select_autoescape()
)

# Load all the files from PROMPT_TEMPLATES_DIR that end with .jinja
PROMPT_TEMPLATE_LIST = [
Expand Down Expand Up @@ -84,5 +87,63 @@ def render_prompt(prompt_name: str, language: str, kwargs: dict[str, Any]) -> st
f"Prompt template {full_prompt_name} not found and no default available"
)

template = env.get_template(full_prompt_name)
template = prompt_env.get_template(full_prompt_name)
return template.render(**kwargs)


JSON_TEMPLATE_LIST = [
f.name for f in os.scandir(JSON_TEMPLATES_DIR) if f.is_file() and f.name.endswith(".jinja")
]

json_env = Environment(loader=FileSystemLoader(JSON_TEMPLATES_DIR), autoescape=select_autoescape())

for name in set(JSON_TEMPLATE_LIST):
logger.info(f"JSON template {name} found in {JSON_TEMPLATES_DIR}")


def render_json(
prompt_name: str,
language: str,
kwargs: dict[str, Any],
# json keys to validate
keys_to_validate: Optional[list[str]] = None,
) -> dict[str, Any]:
"""Render a message template with the given arguments and return a dictionary object.

Args:
prompt_name: Name of the prompt template file (without .jinja extension)
language: ISO 639-1 language code of the prompt template file (example: "en", "nl", "fr", "es", "de". etc.)
kwargs: Dictionary of arguments to pass to the template renderer
keys_to_validate: List of keys to validate in the message

"""
if keys_to_validate is None:
keys_to_validate = []
full_json_template_name = f"{prompt_name}.{language}.jinja"
if full_json_template_name not in JSON_TEMPLATE_LIST:
default_json_template_name = f"{prompt_name}.en.jinja"
if default_json_template_name in JSON_TEMPLATE_LIST:
logger.warning(
f"JSON template {full_json_template_name} not found, using default {default_json_template_name}."
)
full_json_template_name = default_json_template_name
else:
raise ValueError(
f"JSON template {full_json_template_name} not found and no default available"
)
template = json_env.get_template(full_json_template_name)
rendered_prompt = template.render(**kwargs)
try:
message = json.loads(rendered_prompt)
except json.JSONDecodeError as e:
logger.error(f"Failed to parse JSON from rendered prompt: {rendered_prompt}")
raise ValueError(f"Error: {e}") from e

missing_keys = [key for key in keys_to_validate if key not in message]
if missing_keys:
raise ValueError(
f"Missing keys in message: {missing_keys}. Please check the prompt template: {prompt_name}. \n"
f"Message: {message}"
)

return message
57 changes: 7 additions & 50 deletions echo/server/dembrane/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
RUNPOD_TOPIC_MODELER_API_KEY,
)
from dembrane.sentry import init_sentry
from dembrane.prompts import render_json
from dembrane.directus import (
DirectusBadRequest,
DirectusServerError,
Expand Down Expand Up @@ -610,61 +611,17 @@ def task_create_project_library(project_id: str, language: str) -> None:
logger.error(f"Can retry. Failed to create project analysis run: {e}")
raise e from e

DEFAULT_PROMPTS = {
"en": [
{
"user_query": "Power Plays",
"user_query_context": "Identify who drives conversations, influences decisions, and shapes outcomes. Focus on detecting authority patterns, persuasion dynamics, and organizational hierarchy signals. Analyze speaking time distribution, interruption patterns, and whose ideas get adopted or dismissed. Map the power architecture of decision-making processes. Expected aspects: 3-7 for small datasets, 5-12 for medium datasets, 8-15 for large datasets. Processing hint: Look for distinct power centers, influence networks, and decision-making bottlenecks. Quality threshold: Each aspect should represent a unique power dynamic with clear behavioral evidence.",
},
{
"user_query": "Smart vs. Loud",
"user_query_context": "Distinguish between evidence-backed arguments and opinion-based statements. Identify participants who support claims with data, examples, and logical reasoning versus those relying on volume, authority, or emotional appeals. Measure argument quality, source credibility, and reasoning depth across different speakers. Expected aspects: 2-5 for small datasets, 4-8 for medium datasets, 6-12 for large datasets. Processing hint: Focus on argument structure, evidence types, and persuasion mechanisms. Quality threshold: Each aspect should demonstrate clear qualitative differences in reasoning approaches.",
},
{
"user_query": "Mind Changes",
"user_query_context": "Track position shifts, consensus formation, and persuasion effectiveness throughout conversations. Identify moments where participants change their stance, what triggers these shifts, and which arguments successfully influence others. Map the evolution from initial disagreement to final alignment or persistent division. Expected aspects: 2-6 for small datasets, 4-10 for medium datasets, 6-14 for large datasets. Processing hint: Identify temporal patterns, trigger events, and persuasion pathways. Quality threshold: Each aspect should show distinct change mechanisms with clear before/after states.",
},
{
"user_query": "Trust Signals",
"user_query_context": "Analyze what evidence types, sources, and expertise different participants find credible and compelling. Identify which data sources carry weight, whose opinions influence others, and how different groups validate information. Reveal the implicit credibility hierarchy that drives decision-making. Expected aspects: 3-6 for small datasets, 5-10 for medium datasets, 7-13 for large datasets. Processing hint: Map credibility networks, authority recognition patterns, and validation processes. Quality threshold: Each aspect should represent distinct credibility criteria with supporting behavioral evidence.",
},
{
"user_query": "Getting Stuff Done",
"user_query_context": "Measure conversation momentum toward actionable outcomes. Track progression from problem identification to concrete solutions, next steps, and ownership assignment. Identify which discussions generate accountability versus those that circle without resolution. Focus on decision-making effectiveness and implementation planning. Expected aspects: 2-5 for small datasets, 3-8 for medium datasets, 5-11 for large datasets. Processing hint: Focus on resolution patterns, accountability mechanisms, and implementation pathways. Quality threshold: Each aspect should demonstrate measurable progress toward concrete outcomes.",
},
],
"nl": [
{
"user_query": "Wie heeft de touwtjes in handen",
"user_query_context": "Kijk wie de gesprekken stuurt, beslissingen beïnvloedt en de uitkomsten bepaalt. Let op wie het meeste spreekt, wie anderen onderbreekt en wiens ideeën worden opgepakt. Breng in kaart hoe de machtsbalans werkt en wie echt de lakens uitdeelt. Verwachte aspecten: 3-7 voor kleine datasets, 5-12 voor middelgrote datasets, 8-15 voor grote datasets. Verwerkingstip: Zoek naar duidelijke machtscentra, invloedsnetwerken en besluitvormingsknelpunten. Kwaliteitsdrempel: Elk aspect moet een unieke machtsdynamiek tonen met duidelijk gedragsbewijs.",
},
{
"user_query": "Slim praten vs. hard praten",
"user_query_context": "Onderscheid tussen argumenten met bewijs en loze praatjes. Wie ondersteunt hun punt met data en voorbeelden? Wie vertrouwt vooral op volume, status of emoties? Ontdek het verschil tussen echte inhoud en veel lawaai maken. Verwachte aspecten: 2-5 voor kleine datasets, 4-8 voor middelgrote datasets, 6-12 voor grote datasets. Verwerkingstip: Focus op argumentstructuur, bewijstypen en overtuigingsmechanismen. Kwaliteitsdrempel: Elk aspect moet duidelijke kwaliteitsverschillen in redeneerbenaderingen tonen.",
},
{
"user_query": "Van mening veranderen",
"user_query_context": "Volg wie van standpunt wisselt tijdens gesprekken. Wat zorgt ervoor dat mensen hun mening bijstellen? Welke argumenten werken echt? Zie hoe discussies evoleren van onenigheid naar overeenstemming (of juist niet). Verwachte aspecten: 2-6 voor kleine datasets, 4-10 voor middelgrote datasets, 6-14 voor grote datasets. Verwerkingstip: Identificeer tijdspatronen, trigger events en overtuigingstrajecten. Kwaliteitsdrempel: Elk aspect moet duidelijke veranderingsmechanismen tonen met voor/na situaties.",
},
{
"user_query": "Wat mensen geloven",
"user_query_context": "Analyseer waar verschillende mensen op vertrouwen. Welke bronnen vinden ze geloofwaardig? Wiens mening telt? Hoe valideren verschillende groepen informatie? Ontdek de onzichtbare geloofwaardigheidshiërarchie die beslissingen stuurt. Verwachte aspecten: 3-6 voor kleine datasets, 5-10 voor middelgrote datasets, 7-13 voor grote datasets. Verwerkingstip: Breng geloofwaardigheidsnetwerken, autoriteitsherkenning en validatieprocessen in kaart. Kwaliteitsdrempel: Elk aspect moet verschillende geloofwaardigheidscriteria tonen met gedragsbewijs.",
},
{
"user_query": "Shit voor elkaar krijgen",
"user_query_context": "Meet hoe gesprekken leiden tot echte actie. Gaan discussies van probleem naar oplossing? Wie pakt wat op? Welke gesprekken leiden tot resultaat en welke draaien maar rond zonder uitkomst? Focus op wat er daadwerkelijk gebeurt na het praten. Verwachte aspecten: 2-5 voor kleine datasets, 3-8 voor middelgrote datasets, 5-11 voor grote datasets. Verwerkingstip: Focus op oplossingspatronen, verantwoordelijkheidsmechanismen en implementatietrajecten. Kwaliteitsdrempel: Elk aspect moet meetbare vooruitgang naar concrete resultaten tonen.",
},
],
}

default_view_name_list = ["default_view_recurring_themes"]
messages = []

for prompt in DEFAULT_PROMPTS[language]:
for view_name in default_view_name_list:
message = render_json(view_name, language, {}, ["user_query", "user_query_context"])
logger.info(f"Message: {message}")
messages.append(
task_create_view.message(
project_analysis_run_id=new_run_id,
user_query=prompt["user_query"],
user_query_context=prompt["user_query_context"],
user_query=message["user_query"],
user_query_context=message["user_query_context"],
language=language,
)
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"user_query": "Geben Sie einen Überblick über die Hauptthemen und wiederkehrenden Themen",
"user_query_context": "Identifizieren Sie wiederkehrende Themen, Themen und Argumente, die konsistent in den Gesprächen auftreten. Analysieren Sie deren Häufigkeit, Intensität und Konsistenz. Erwartete Ausgabe: 3-7 Aspekte für kleine Datensätze, 5-12 für mittlere Datensätze, 8-15 für große Datensätze. Verarbeitungsrichtlinien: Konzentrieren Sie sich auf eindeutige Muster, die in mehreren Gesprächen auftreten. Qualitätsschwelle: Jeder Aspekt muss ein einzigartiges, konsistent auftretendes Thema mit klaren Belegen für Wiederholung darstellen."
}
Comment on lines +1 to +4
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Nit: trim trailing space after closing brace

Tiny whitespace quirk—there’s a " } " with a trailing space. Won’t break JSON, but keeping templates surgically clean avoids diff noise later.

-} 
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{
"user_query": "Geben Sie einen Überblick über die Hauptthemen und wiederkehrenden Themen",
"user_query_context": "Identifizieren Sie wiederkehrende Themen, Themen und Argumente, die konsistent in den Gesprächen auftreten. Analysieren Sie deren Häufigkeit, Intensität und Konsistenz. Erwartete Ausgabe: 3-7 Aspekte für kleine Datensätze, 5-12 für mittlere Datensätze, 8-15 für große Datensätze. Verarbeitungsrichtlinien: Konzentrieren Sie sich auf eindeutige Muster, die in mehreren Gesprächen auftreten. Qualitätsschwelle: Jeder Aspekt muss ein einzigartiges, konsistent auftretendes Thema mit klaren Belegen für Wiederholung darstellen."
}
{
"user_query": "Geben Sie einen Überblick über die Hauptthemen und wiederkehrenden Themen",
"user_query_context": "Identifizieren Sie wiederkehrende Themen, Themen und Argumente, die konsistent in den Gesprächen auftreten. Analysieren Sie deren Häufigkeit, Intensität und Konsistenz. Erwartete Ausgabe: 3-7 Aspekte für kleine Datensätze, 5-12 für mittlere Datensätze, 8-15 für große Datensätze. Verarbeitungsrichtlinien: Konzentrieren Sie sich auf eindeutige Muster, die in mehreren Gesprächen auftreten. Qualitätsschwelle: Jeder Aspekt muss ein einzigartiges, konsistent auftretendes Thema mit klaren Belegen für Wiederholung darstellen."
}
🤖 Prompt for AI Agents
In echo/server/json_templates/default_view_recurring_themes.de.jinja at lines 1
to 4, remove the trailing space after the closing brace in the JSON object to
keep the template clean and avoid unnecessary diff noise.

Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"user_query": "Provide an overview of the main topics and recurring themes",
"user_query_context": "Identify recurring themes, topics, and arguments that appear consistently across conversations. Analyze their frequency, intensity, and consistency. Expected output: 3-7 aspects for small datasets, 5-12 for medium datasets, 8-15 for large datasets. Processing guidance: Focus on distinct patterns that emerge across multiple conversations. Quality threshold: Each aspect must represent a unique, consistently appearing theme with clear evidence of recurrence."
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"user_query": "Proporcione una visión general de los temas principales y temas recurrentes",
"user_query_context": "Identifique temas recurrentes, tópicos y argumentos que aparecen consistentemente a través de las conversaciones. Analice su frecuencia, intensidad y consistencia. Salida esperada: 3-7 aspectos para conjuntos de datos pequeños, 5-12 para conjuntos de datos medianos, 8-15 para conjuntos de datos grandes. Guía de procesamiento: Enfóquese en patrones distintos que emergen a través de múltiples conversaciones. Umbral de calidad: Cada aspecto debe representar un tema único y consistentemente presente con evidencia clara de recurrencia."
}
Comment on lines +1 to +4
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Final brace whitespace

Ditto—remove the extra space for a squeaky-clean tree.

-} 
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{
"user_query": "Proporcione una visión general de los temas principales y temas recurrentes",
"user_query_context": "Identifique temas recurrentes, tópicos y argumentos que aparecen consistentemente a través de las conversaciones. Analice su frecuencia, intensidad y consistencia. Salida esperada: 3-7 aspectos para conjuntos de datos pequeños, 5-12 para conjuntos de datos medianos, 8-15 para conjuntos de datos grandes. Guía de procesamiento: Enfóquese en patrones distintos que emergen a través de múltiples conversaciones. Umbral de calidad: Cada aspecto debe representar un tema único y consistentemente presente con evidencia clara de recurrencia."
}
{
"user_query": "Proporcione una visión general de los temas principales y temas recurrentes",
"user_query_context": "Identifique temas recurrentes, tópicos y argumentos que aparecen consistentemente a través de las conversaciones. Analice su frecuencia, intensidad y consistencia. Salida esperada: 3-7 aspectos para conjuntos de datos pequeños, 5-12 para conjuntos de datos medianos, 8-15 para conjuntos de datos grandes. Guía de procesamiento: Enfóquese en patrones distintos que emergen a través de múltiples conversaciones. Umbral de calidad: Cada aspecto debe representar un tema único y consistentemente presente con evidencia clara de recurrencia."
}
🤖 Prompt for AI Agents
In echo/server/json_templates/default_view_recurring_themes.es.jinja at lines 1
to 4, remove the extra space after the closing brace of the JSON object to
ensure there is no trailing whitespace, keeping the file clean and consistent.

Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"user_query": "Fournissez un aperçu des sujets principaux et des thèmes récurrents",
"user_query_context": "Identifiez les thèmes récurrents, sujets et arguments qui apparaissent de manière cohérente à travers les conversations. Analysez leur fréquence, intensité et cohérence. Sortie attendue : 3-7 aspects pour les petits ensembles de données, 5-12 pour les ensembles moyens, 8-15 pour les grands ensembles. Guidance de traitement : Concentrez-vous sur les modèles distincts qui émergent à travers plusieurs conversations. Seuil de qualité : Chaque aspect doit représenter un thème unique et cohérent avec des preuves claires de récurrence."
}
Comment on lines +1 to +4
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Whitespace déjà-vu

Same trailing space issue as the German template—zap it for consistency.

-} 
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{
"user_query": "Fournissez un aperçu des sujets principaux et des thèmes récurrents",
"user_query_context": "Identifiez les thèmes récurrents, sujets et arguments qui apparaissent de manière cohérente à travers les conversations. Analysez leur fréquence, intensité et cohérence. Sortie attendue : 3-7 aspects pour les petits ensembles de données, 5-12 pour les ensembles moyens, 8-15 pour les grands ensembles. Guidance de traitement : Concentrez-vous sur les modèles distincts qui émergent à travers plusieurs conversations. Seuil de qualité : Chaque aspect doit représenter un thème unique et cohérent avec des preuves claires de récurrence."
}
{
"user_query": "Fournissez un aperçu des sujets principaux et des thèmes récurrents",
"user_query_context": "Identifiez les thèmes récurrents, sujets et arguments qui apparaissent de manière cohérente à travers les conversations. Analysez leur fréquence, intensité et cohérence. Sortie attendue : 3-7 aspects pour les petits ensembles de données, 5-12 pour les ensembles moyens, 8-15 pour les grands ensembles. Guidance de traitement : Concentrez-vous sur les modèles distincts qui émergent à travers plusieurs conversations. Seuil de qualité : Chaque aspect doit représenter un thème unique et cohérent avec des preuves claires de récurrence."
}
🤖 Prompt for AI Agents
In echo/server/json_templates/default_view_recurring_themes.fr.jinja at lines 1
to 4, remove the trailing space at the end of the JSON content to maintain
consistency with other language templates and avoid whitespace issues.

Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"user_query": "Geef een overzicht van de hoofdonderwerpen en terugkerende thema's",
"user_query_context": "Identificeer terugkerende thema's, onderwerpen en argumenten die consistent voorkomen in gesprekken. Analyseer hun frequentie, intensiteit en consistentie. Verwachte uitvoer: 3-7 aspecten voor kleine datasets, 5-12 voor middelgrote datasets, 8-15 voor grote datasets. Verwerkingsrichtlijnen: Focus op onderscheidende patronen die opkomen in meerdere gesprekken. Kwaliteitsdrempel: Elk aspect moet een uniek, consistent voorkomend thema vertegenwoordigen met duidelijk bewijs van herhaling."
}
Comment on lines +1 to +4
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Trailing space tidy-up

Nip the space after the closing brace.

-} 
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{
"user_query": "Geef een overzicht van de hoofdonderwerpen en terugkerende thema's",
"user_query_context": "Identificeer terugkerende thema's, onderwerpen en argumenten die consistent voorkomen in gesprekken. Analyseer hun frequentie, intensiteit en consistentie. Verwachte uitvoer: 3-7 aspecten voor kleine datasets, 5-12 voor middelgrote datasets, 8-15 voor grote datasets. Verwerkingsrichtlijnen: Focus op onderscheidende patronen die opkomen in meerdere gesprekken. Kwaliteitsdrempel: Elk aspect moet een uniek, consistent voorkomend thema vertegenwoordigen met duidelijk bewijs van herhaling."
}
{
"user_query": "Geef een overzicht van de hoofdonderwerpen en terugkerende thema's",
"user_query_context": "Identificeer terugkerende thema's, onderwerpen en argumenten die consistent voorkomen in gesprekken. Analyseer hun frequentie, intensiteit en consistentie. Verwachte uitvoer: 3-7 aspecten voor kleine datasets, 5-12 voor middelgrote datasets, 8-15 voor grote datasets. Verwerkingsrichtlijnen: Focus op onderscheidende patronen die opkomen in meerdere gesprekken. Kwaliteitsdrempel: Elk aspect moet een uniek, consistent voorkomend thema vertegenwoordigen met duidelijk bewijs van herhaling."
}
🤖 Prompt for AI Agents
In echo/server/json_templates/default_view_recurring_themes.nl.jinja at lines 1
to 4, remove the trailing space after the closing brace to tidy up the JSON
template and ensure there is no unnecessary whitespace at the end of the file.

Loading