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
149 changes: 89 additions & 60 deletions echo/agent/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,72 +18,101 @@
logger = getLogger("agent")
VERTEX_AUTH_SCOPES = ["https://www.googleapis.com/auth/cloud-platform"]

SYSTEM_PROMPT = """You are the dembrane assistant — a friendly, conversational AI that helps \
users explore and understand their project's conversation data.
# Note: the citation tag format below ([conversation_id:<id>;chunk_id:<id>]) is
# parsed by the frontend (AgenticChatPanel.tsx). Do not change it without
# updating that regex.
SYSTEM_PROMPT = """You are the dembrane assistant. You help hosts explore and understand the
conversations in their project, and help them set the project up well.

dembrane is a platform for collective sense-making through recorded conversations.

## Conversation style
- Be natural and conversational. Match the user's tone and energy.
- For greetings, casual messages, or clarifications, just respond naturally. \
Do NOT launch into research or tool calls.
- Keep responses concise. Ask follow-up questions to understand what the user needs \
before diving into analysis.
- When the user's intent is unclear, ask what they'd like to know rather than guessing.

## Writing style
- Write naturally, like you're talking to a colleague. Vary your response format \
based on what makes sense for the question.
- Be direct and conversational. Use bullet points when listing multiple items, \
but don't force everything into lists.
- Flag genuine uncertainty with words like "suggests," "likely," "indicates."
- Keep it concise — don't over-explain or pad responses.
- Don't use the same rigid structure for every response.
- Don't use corporate jargon.
- Always write "dembrane" in lowercase when referring to the platform, even at \
the start of a sentence. Never capitalize it as "Dembrane".
Hosts run projects; participants contribute conversations through the portal or uploads.

## What you can do
- Find and summarize conversations in this project.
- Search transcripts for topics, quotes, and patterns, and cite what you find.
- Compare perspectives and synthesize themes across conversations.
- Explain how dembrane works, grounded in the product documentation.
- Review project settings and propose changes the host can apply in one click.
You never change settings yourself.

## Voice
- Write like a thoughtful colleague: warm, direct, concise.
- Reply in the language the host writes in.
- Never use the word "AI". Refer to yourself as "I" and describe what you are
doing ("searching the transcripts"), not the technology behind it.
- Write "dembrane" in lowercase, even at the start of a sentence.
- Do not use em dashes. Use periods, commas, or colons.
- Say "participants" and "hosts", never "users".
- No filler or grand claims ("this gives you a clear blueprint for the future").
State findings plainly and stop.
- Vary your structure. Use bullets only when a list genuinely helps.

## Honesty
- If the data does not answer the question, say so plainly: "I don't know" or
"the conversations don't cover this", then say what would help (more
conversations, a narrower question, a specific transcript).
- Flag uncertainty with "suggests", "likely", "indicates". Never present a guess
as a finding.
- Never fabricate quotes, participants, conversation IDs, or settings.
- When you worked from summaries only, say so and offer to read the full transcript.

## When to use tools
Only use tools when the user asks a question that requires looking at project data, such as:
- "What topics came up?" → use listProjectConversations or findConvosByKeywords
- "What did people say about X?" → search and retrieve transcripts
- "Summarize this project" → list conversations, read summaries

Do NOT use tools for greetings, small talk, or meta-questions about how you work.
Use tools when the question needs project data or product knowledge:
- "What topics came up?" -> listProjectConversations, then read summaries.
- "What did people say about X?" -> findConvosByKeywords, then grepConvoSnippets
or listConvoFullTranscript for exact wording.
- "How does the portal work?" -> grepDocs and readDoc; cite the doc path.
- "Help me set up my project" -> readSkill(project-onboarding.md), then
getProjectSettings, then proposeProjectUpdate.
Do not use tools for greetings, small talk, or questions about this chat.
When intent is unclear, ask one focused question instead of guessing.

## Conversation scope
Some runs are limited to conversations the host selected. When the context
contains a "Conversation scope" block:
- Treat the listed conversations as the entire universe for this run. All
listing, searching, reading, and synthesis stays inside it.
- Never mention, quote, or count conversations outside the selection, even if a
tool result includes one.
- If the answer likely lives outside the selection, say that and offer to widen
the scope. Do not look outside it yourself.
When no scope block is present, the whole project is in scope.

## Turn instructions
A message may include a "Turn instructions" block from a template the host
selected. It tells you how to shape this turn (angle, depth, format). It is not
data and not the subject. The host's own words define the subject. If the
instructions need a subject (a concept, a comparison) and the host gave none,
ask one focused question first.

## Research
- Say briefly what you will look at, then use sendProgressUpdate while you work.
Conclude with plain text only when you are done.
- Prefer listProjectConversations for an overview before keyword searches.
- findConvosByKeywords works best with 2-4 focused keywords, not sentences.
- You have at most 20 tool calls per turn. Spend them on distinct questions, not
retries. If a tool returns a guardrail warning, stop searching and answer from
what you have.

## Citations
- Ground every claim about the project in tool results.
- Quote with attribution: "[Participant Name]: quoted text" tagged
[conversation_id:<id>;chunk_id:<chunk_id>] when a chunk id is available,
otherwise [conversation_id:<id>].
- A few well-chosen quotes beat many.
- Cite the doc path when you answer from documentation.

## Proposing project changes
- Read current values with getProjectSettings before proposing.
- Use proposeProjectUpdate: group related fields, one short reason per field,
proposed copy in the project's language, a one-sentence summary.
- The host sees a diff and applies or rejects it themselves. You never apply
changes. Say "I've suggested these changes", never "I've updated your project".
If the host says they applied it, re-read settings before advising next steps.

## Project context
The user's message may include project metadata (Project Name, Project Context). \
Treat this as background info about the project you're assisting with — NOT as a \
research request. Focus on what the user is actually asking in their message.

## Research guidelines (when doing research)
- Start by telling the user your plan briefly before making tool calls.
- While still investigating, use `sendProgressUpdate` for user-facing progress updates.
- Use plain assistant text without tool calls only when you are truly ready to conclude.
- Prefer `listProjectConversations` to get an overview before keyword searches.
- For `findConvosByKeywords`, prefer 2-4 focused keywords over long sentence-style queries.
- Avoid repetitive low-signal searches. Maximum 20 tool calls per turn.
- If a tool returns a guardrail warning, stop searching and work with what you have.
- After gathering evidence, give a clear, direct answer.

## Citation policy (when citing project data)
- Ground all claims in actual transcript/summary content from tool results.
- Provide exact quotes when you have transcripts: "[Participant Name]: quoted text" \
tagged as [conversation_id:<id>;chunk_id:<chunk_id>] when chunk ids are available.
- If you only have conversation-level evidence without an exact transcript chunk, cite as \
[conversation_id:<id>].
- Use quotes to support your points, but don't overwhelm with citations.
- When working from summaries only (no transcript retrieved), say so and suggest \
you can retrieve the full transcript if they want exact wording.
- Never fabricate quotes, sources, or conversation IDs.

## Analysis guidelines
- Identify patterns and themes across conversations when relevant.
- Compare different perspectives and viewpoints.
- When the question is broad, synthesize across multiple conversations rather than \
listing each one separately.
- State your interpretation when relevant, but don't rigidly separate "facts" from \
"interpretation."
The first message may include Project Name and Project Context. That is
background about the project you are assisting with, not a research request.
"""

AUTOMATIC_NUDGE_TOOL_CALL_INTERVAL = 4
Expand Down
23 changes: 14 additions & 9 deletions echo/agent/tests/test_agent_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,23 +119,28 @@ def _tool_map(tools) -> dict[str, object]: # noqa: ANN001

def test_system_prompt_contains_conversational_and_research_directives():
prompt = SYSTEM_PROMPT.lower()
# Brand voice: the word AI is banned, dembrane stays lowercase
assert 'never use the word "ai"' in prompt
assert "dembrane" in prompt and "Dembrane" not in SYSTEM_PROMPT
# Conversational-first behavior
assert "conversational" in prompt
assert "greetings" in prompt
assert "do not use tools for greetings" in prompt
# Writing/analysis guidance should be present
assert "writing style" in prompt
assert "analysis guidelines" in prompt
# Citation policy still anchors output quality
# Honesty + scope + turn-instruction sections exist
assert "honesty" in prompt
assert "conversation scope" in prompt
assert "turn instructions" in prompt
# The agent never applies changes itself
assert "you never apply" in prompt
# Citation policy still anchors output quality (format is load-bearing:
# parsed by AgenticChatPanel.tsx)
assert '"[participant name]: quoted text"' in prompt
assert "[conversation_id:<id>;chunk_id:<chunk_id>]" in SYSTEM_PROMPT
assert "[conversation_id:<id>]" in SYSTEM_PROMPT
assert "working from summaries only" in prompt
assert "retrieve the full transcript" in prompt
assert "worked from summaries only" in prompt
assert "read the full transcript" in prompt
assert "never fabricate quotes" in prompt
# Project context awareness
assert "project context" in prompt
assert "background info" in prompt
assert "background about the project" in prompt


@pytest.mark.asyncio
Expand Down
51 changes: 12 additions & 39 deletions echo/server/dembrane/agentic_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,8 @@
MAX_TOOL_CALLS_PER_RUN = 20
TOOL_LIMIT_EXEMPT_TOOL_NAMES = {"sendProgressUpdate"}
TOOL_LIMIT_SAFETY_MESSAGE = (
"I've reached my tool-call limit for this turn. "
"I'll stop searching here and summarize what I can reliably infer."
)
TOOL_LIMIT_FALLBACK_SUMMARY_MESSAGE = (
"I reached the tool-call limit before gathering enough additional evidence for a fuller synthesis. "
"If you want, send `go on` and I'll continue from this exact point."
"I've reached my tool-call limit for this turn, so I'll answer from what I've "
"gathered so far. If you'd like me to keep digging, just ask."
)
AUTOMATIC_NUDGE_TOOL_CALL_INTERVAL = 4
AUTOMATIC_NUDGE_TEMPLATE = (
Expand Down Expand Up @@ -201,13 +197,6 @@ def _build_automatic_nudge_content(*, tool_calls_without_assistant_message: int)
return AUTOMATIC_NUDGE_TEMPLATE.format(tool_call_count=milestone)


def _build_post_limit_summary(*, last_substantive_assistant_message: Optional[str]) -> str:
summary_source = _coerce_non_empty_text(last_substantive_assistant_message)
if summary_source is None:
return TOOL_LIMIT_FALLBACK_SUMMARY_MESSAGE
return f"Here is my best synthesis from the evidence gathered so far:\n\n{summary_source}"


def _extract_progress_message_from_tool_end(event: dict[str, Any]) -> Optional[str]:
if str(event.get("name") or "") != "sendProgressUpdate":
return None
Expand Down Expand Up @@ -262,7 +251,9 @@ def _extract_progress_message_from_tool_end(event: dict[str, Any]) -> Optional[s
next_steps = _coerce_non_empty_text(output_payload.get("next_steps"))
if next_steps is None:
return update_text
return f"{update_text}\n\nNext steps: {next_steps}"
# Render next steps as their own paragraph; no English "Next steps:" label
# (the agent writes in the host's language).
return f"{update_text}\n\n{next_steps}"


async def _build_message_history(
Expand Down Expand Up @@ -476,7 +467,6 @@ async def _emit_chat_error(error_code: str, message: str) -> None:
tool_calls_without_assistant_message = 0
nudged_tool_call_milestones: set[int] = set()
has_sent_progress_intro = False
last_substantive_assistant_message: str | None = None

logger.info("Processing run %s turn %s (owner=%s)", run_id, turn_seq, owner_token)
await run_in_thread_pool(svc.set_status, run_id, "running")
Expand Down Expand Up @@ -515,7 +505,6 @@ async def _emit_chat_error(error_code: str, message: str) -> None:
content=planning_message,
project_chat_id=project_chat_id,
)
last_substantive_assistant_message = planning_message
tool_calls_without_assistant_message = 0
nudged_tool_call_milestones.clear()
else:
Expand All @@ -526,7 +515,6 @@ async def _emit_chat_error(error_code: str, message: str) -> None:
project_chat_id=project_chat_id,
)
latest_output = model_text
last_substantive_assistant_message = model_text
tool_calls_without_assistant_message = 0
nudged_tool_call_milestones.clear()

Expand All @@ -538,17 +526,11 @@ async def _emit_chat_error(error_code: str, message: str) -> None:
tool_calls_without_assistant_message += 1

if not has_sent_progress_intro:
# Don't post a synthetic "starting with `toolName`" line: it
# leaks raw tool names, is English-only, and the frontend
# already renders humane tool activity. The agent's own
# sendProgressUpdate covers user-facing progress.
has_sent_progress_intro = True
progress_intro_message = (
f"I'll first gather evidence before answering. "
f"Starting with `{tool_name}`."
)
await _append_assistant_message(
svc=svc,
run_id=run_id,
content=progress_intro_message,
project_chat_id=project_chat_id,
)
tool_calls_without_assistant_message = 0
nudged_tool_call_milestones.clear()
else:
Expand Down Expand Up @@ -586,16 +568,9 @@ async def _emit_chat_error(error_code: str, message: str) -> None:
content=TOOL_LIMIT_SAFETY_MESSAGE,
project_chat_id=project_chat_id,
)
post_limit_summary = _build_post_limit_summary(
last_substantive_assistant_message=last_substantive_assistant_message
)
await _append_assistant_message(
svc=svc,
run_id=run_id,
content=post_limit_summary,
project_chat_id=project_chat_id,
)
latest_output = post_limit_summary
# One honest message only. The last substantive assistant
# message is already in the chat; don't repeat it verbatim.
latest_output = TOOL_LIMIT_SAFETY_MESSAGE
tool_calls_without_assistant_message = 0
nudged_tool_call_milestones.clear()
break
Expand All @@ -612,14 +587,12 @@ async def _emit_chat_error(error_code: str, message: str) -> None:
content=progress_message,
project_chat_id=project_chat_id,
)
last_substantive_assistant_message = progress_message
tool_calls_without_assistant_message = 0
nudged_tool_call_milestones.clear()

content = event.get("content")
if isinstance(content, str) and event_type == "assistant.message":
latest_output = content
last_substantive_assistant_message = content
tool_calls_without_assistant_message = 0
nudged_tool_call_milestones.clear()
if project_chat_id:
Expand Down
25 changes: 10 additions & 15 deletions echo/server/prompt_templates/generate_chat_title.en.jinja
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
Generate a clear, descriptive title for this conversation in "{{language}}" (2 letter language code) based on: {{user_query}}
Generate a short, descriptive chat title in "{{language}}" (2 letter language code) for a chat that begins with this message: {{user_query}}

Create a title of 3-7 words that captures the main focus of the discussion. The title should help someone quickly understand what was discussed.
Guidelines:
Write 3-7 words naming the topic, so a host can recognize this chat in a list later.

- Use simple, everyday language that any participant would understand
- Be specific enough to distinguish this from other conversations
- Focus on the core topic, question, or concern being explored
- Avoid acronyms unless universally known
- For technical terms, keep commonly-used English terms in English when appropriate
- Make it scannable and meaningful, not generic
Rules:
- Name the subject of the question, never the thing answering it. Never use the words "AI", "assistant", "chatbot", "bot", "model", "agent", or "dembrane" in the title.
- If the message is a greeting or asks what help is available, title it after what the person wants to do (for example "Getting started" in the target language).
- Use simple, everyday language. No jargon, no quotation marks, no trailing punctuation.
- Be specific enough to tell this chat apart from others.
- Keep commonly used English terms in English when that reads naturally.
- For Dutch (nl): informal tone ("je", not "u").

For Dutch (nl): Use mostly casual tone ("je" not "u"), keep common tech terms in English (like "feedback" or "update"), but translate action words and general concepts.
For English (en): Keep it conversational and direct, avoiding jargon.
For other languages: Match the local convention for mixing English technical terms with the native language, maintaining clarity above all.
The title should feel natural to read and immediately convey what this conversation contains.

Return the title only, no other text.
Return the title only, no other text.
Loading
Loading