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
9 changes: 8 additions & 1 deletion apps/project-assistant/backend/app/routers/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,14 +155,21 @@ def image_to_base64(image_path: Path) -> dict:


def save_assistant_message(
db, project_id: str, project_title: str, content: str, msg_date: datetime
db,
project_id: str,
project_title: str,
content: str,
version: int,
msg_date: datetime,
) -> None:
"""Save an assistant response message."""
db.messages.insert_one(
{
"project_id": project_id,
"project_title": project_title,
"user_id": USER_ID,
"role": "assistant",
"version": version,
"content": content,
"created_at": msg_date,
}
Expand Down
12 changes: 5 additions & 7 deletions apps/project-assistant/backend/app/routers/routes.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import json
import logging
from datetime import datetime
from typing import Optional
Expand Down Expand Up @@ -59,21 +58,20 @@ def send_message(

def stream_and_save():
response_content = ""
for chunk in generate_response(conversation, memories=memories):
yield json.dumps(chunk) + "\n"
if chunk["type"] == "response":
response_content += chunk["content"]
for text in generate_response(conversation, memories=memories):
yield text
response_content += text

# Save messages to DB after streaming completes
if content:
save_user_message(db, project_id, project_title, content, version, msg_date)
for path in image_paths:
save_user_message(db, project_id, project_title, path, version, msg_date)
save_assistant_message(
db, project_id, project_title, response_content, msg_date
db, project_id, project_title, response_content, version, msg_date
)

return StreamingResponse(stream_and_save(), media_type="application/x-ndjson")
return StreamingResponse(stream_and_save(), media_type="text/plain")


@router.get("/search")
Expand Down
19 changes: 5 additions & 14 deletions apps/project-assistant/backend/app/services/anthropic.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def extract_memories(user_message: str) -> list[dict]:
response = client.beta.messages.parse(
model=ANTHROPIC_MODEL,
max_tokens=2000,
temperature=1,
temperature=0,
betas=["structured-outputs-2025-11-13"],
system=MEMORY_EXTRACTION_PROMPT,
messages=[{"role": "user", "content": user_message}],
Expand All @@ -50,7 +50,7 @@ def extract_memories(user_message: str) -> list[dict]:


def generate_response(messages: list[dict], memories: Optional[list[str]] = None):
"""Generate a response with extended thinking."""
"""Generate a streaming response."""
logger.info(
f"Generating response using {ANTHROPIC_MODEL} with {len(memories) if memories else 0} memories"
)
Expand All @@ -62,18 +62,9 @@ def generate_response(messages: list[dict], memories: Optional[list[str]] = None

with client.messages.stream(
model=ANTHROPIC_MODEL,
max_tokens=16000,
temperature=1,
thinking={
"type": "enabled",
"budget_tokens": 8000,
},
max_tokens=4096,
temperature=0,
system=system_prompt,
messages=messages,
) as stream:
for event in stream:
if event.type == "content_block_delta":
if hasattr(event.delta, "thinking"):
yield {"type": "thinking", "content": event.delta.thinking}
elif hasattr(event.delta, "text"):
yield {"type": "response", "content": event.delta.text}
yield from stream.text_stream
22 changes: 12 additions & 10 deletions apps/project-assistant/backend/app/services/prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,22 +23,24 @@
]"""


SYSTEM_PROMPT = """You are an AI-powered developer productivity assistant.
SYSTEM_PROMPT = """You are an AI-powered project-planning assistant.
Your role is to help developers plan and break down projects into actionable steps.

IMPORTANT: When context about the user's preferences or past decisions is provided, reference them naturally to maintain consistency.
IMPORTANT: When context about the user's preferences or past decisions is provided, actively reference them in your response.

Guidelines:
- Help break down projects into smaller, manageable tasks
- Break down projects into smaller, manageable tasks
- Ask clarifying questions about requirements, tech stack, and constraints
- Suggest best practices and potential approaches
- Identify dependencies and potential blockers early
- Reference past preferences (e.g., "You typically prefer TypeScript - should we use that here?")
- Actively reference past preferences
- Keep responses concise and actionable

Formatting:
- Use plain text and bullet points only - no headers or titles
- Keep it conversational and direct
- Use numbered lists for sequential steps
STRICT Formatting Rules:
- NO headers, titles, or section labels
- NO markdown formatting (no **, #, etc.)
- Use simple bullet points with dashes (-)
- Use numbered lists (1. 2. 3.) only for sequential steps
- Write in plain conversational text
- Keep paragraphs short

Remember: You're a planning assistant. Help developers think through their projects systematically."""
Remember, your goal is to assist developers in planning their projects effectively while respecting their established preferences and decisions."""
55 changes: 0 additions & 55 deletions apps/project-assistant/frontend/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -821,61 +821,6 @@
display: block;
}

/* Thinking section */
.thinking-section {
margin-bottom: 12px;
}

.thinking-toggle {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
background: var(--mongodb-off-white);
border: 1px solid var(--mongodb-gray-light);
border-radius: 4px;
font-family: inherit;
font-size: 13px;
color: var(--mongodb-slate);
cursor: pointer;
transition: all 0.15s ease;
}

.thinking-toggle:hover {
background: var(--mongodb-white);
border-color: var(--mongodb-dark-green);
color: var(--mongodb-slate);
}

.thinking-toggle.expanded {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
border-bottom-color: transparent;
}

.thinking-icon {
font-size: 10px;
transition: transform 0.15s ease;
}

.thinking-label {
font-weight: 500;
}

.thinking-content {
padding: 12px 16px;
background: var(--mongodb-off-white);
border: 1px solid var(--mongodb-gray-light);
border-top: none;
border-radius: 0 0 4px 4px;
font-size: 13px;
line-height: 1.6;
color: var(--mongodb-slate);
white-space: pre-wrap;
max-height: 300px;
overflow-y: auto;
}

/* Date picker modal */
.date-picker-overlay {
position: fixed;
Expand Down
36 changes: 7 additions & 29 deletions apps/project-assistant/frontend/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,43 +118,21 @@ function App() {
body: formData
})

// Read newline-delimited JSON stream
// Read text stream
const reader = res.body.getReader()
const decoder = new TextDecoder()
let buffer = ''
let thinkingContent = ''
let responseContent = ''

while (true) {
const { done, value } = await reader.read()
if (done) break

buffer += decoder.decode(value, { stream: true })

// Process complete JSON lines
const lines = buffer.split('\n')
buffer = lines.pop() // Keep incomplete line in buffer

for (const line of lines) {
if (!line.trim()) continue
try {
const chunk = JSON.parse(line)
if (chunk.type === 'thinking') {
thinkingContent += chunk.content
} else if (chunk.type === 'response') {
responseContent += chunk.content
}

// Update message with current state
setMessages(prev => prev.map(msg =>
msg._id === aiMessageId
? { ...msg, thinking: thinkingContent, content: responseContent }
: msg
))
} catch (e) {
console.error('Failed to parse chunk:', e)
}
}
responseContent += decoder.decode(value, { stream: true })
setMessages(prev => prev.map(msg =>
msg._id === aiMessageId
? { ...msg, content: responseContent }
: msg
))
}
}

Expand Down
28 changes: 0 additions & 28 deletions apps/project-assistant/frontend/src/components/Entry.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,9 @@ function Entry({ messages, onSendMessage, hasActiveProject, activeProject, proje
const [searchResults, setSearchResults] = useState(null)
const [isSearching, setIsSearching] = useState(false)
const [saveStatus, setSaveStatus] = useState(null)
const [expandedThinking, setExpandedThinking] = useState({})
const messagesEndRef = useRef(null)
const fileInputRef = useRef(null)

const toggleThinking = (msgId) => {
setExpandedThinking(prev => ({
...prev,
[msgId]: !prev[msgId]
}))
}

const handleSaveProject = async () => {
setSaveStatus('saving')
try {
Expand Down Expand Up @@ -179,26 +171,6 @@ function Entry({ messages, onSendMessage, hasActiveProject, activeProject, proje
<div className="message-label">
{msg.role === 'user' ? 'You' : 'Assistant'}
</div>
{msg.thinking && (
<div className="thinking-section">
<button
className={`thinking-toggle ${expandedThinking[msg._id] ? 'expanded' : ''}`}
onClick={() => toggleThinking(msg._id)}
>
<span className="thinking-icon">
{expandedThinking[msg._id] ? '▼' : '▶'}
</span>
<span className="thinking-label">
{expandedThinking[msg._id] ? 'Thinking' : 'Show thinking'}
</span>
</button>
{expandedThinking[msg._id] && (
<div className="thinking-content">
{msg.thinking}
</div>
)}
</div>
)}
{msg.content && (
<div className="message-text">
<ReactMarkdown>{msg.content}</ReactMarkdown>
Expand Down
Loading