Spaces:
Sleeping
Sleeping
Improve perceived and actual response latency
Browse files- Reuse a single httpx.AsyncClient across requests (FastAPI lifespan)
instead of opening a fresh connection to api.anthropic.com on every
message, avoiding a repeated TCP+TLS handshake per turn.
- Stream Claude's reply to the browser as plain text (Anthropic SSE
parsed server-side) instead of waiting for the full completion, so
ARI tool replies and partner messages render incrementally.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- app.py +74 -56
- static/index.html +121 -96
app.py
CHANGED
|
@@ -8,15 +8,31 @@ ARI never becomes the relationship - it augments human-to-human connection.
|
|
| 8 |
Created by Jocelyn Skillman, LMHC
|
| 9 |
"""
|
| 10 |
|
|
|
|
| 11 |
import os
|
|
|
|
|
|
|
| 12 |
import httpx
|
| 13 |
from fastapi import FastAPI, HTTPException
|
| 14 |
from fastapi.staticfiles import StaticFiles
|
| 15 |
-
from fastapi.responses import FileResponse
|
| 16 |
from pydantic import BaseModel
|
| 17 |
from typing import List, Optional
|
| 18 |
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
# Get API key from environment (set as HF Space secret)
|
| 22 |
# Check multiple possible secret names for flexibility
|
|
@@ -504,13 +520,51 @@ class ChatRequest(BaseModel):
|
|
| 504 |
max_tokens: int = 500
|
| 505 |
|
| 506 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 507 |
# ============================================================================
|
| 508 |
# API ENDPOINTS
|
| 509 |
# ============================================================================
|
| 510 |
|
| 511 |
@app.post("/api/tool")
|
| 512 |
async def use_tool(request: ToolRequest):
|
| 513 |
-
"""Process an ARI tool request"""
|
| 514 |
if not ANTHROPIC_API_KEY:
|
| 515 |
raise HTTPException(status_code=500, detail="API key not configured")
|
| 516 |
|
|
@@ -547,66 +601,30 @@ MINIMAL MODE (user wants just the essentials):
|
|
| 547 |
user_message += f"Current stage: {request.stage}\n\n"
|
| 548 |
user_message += request.user_input
|
| 549 |
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
},
|
| 559 |
-
json={
|
| 560 |
-
"model": "claude-sonnet-4-20250514",
|
| 561 |
-
"max_tokens": 1000,
|
| 562 |
-
"temperature": 0.4,
|
| 563 |
-
"system": system_prompt,
|
| 564 |
-
"messages": [{"role": "user", "content": user_message}]
|
| 565 |
-
},
|
| 566 |
-
timeout=60.0
|
| 567 |
-
)
|
| 568 |
-
response.raise_for_status()
|
| 569 |
-
result = response.json()
|
| 570 |
-
return {
|
| 571 |
-
"tool": request.tool,
|
| 572 |
-
"response": result["content"][0]["text"]
|
| 573 |
-
}
|
| 574 |
-
except httpx.HTTPStatusError as e:
|
| 575 |
-
raise HTTPException(status_code=e.response.status_code, detail=str(e))
|
| 576 |
-
except Exception as e:
|
| 577 |
-
raise HTTPException(status_code=500, detail=str(e))
|
| 578 |
|
| 579 |
|
| 580 |
@app.post("/api/chat")
|
| 581 |
async def chat(request: ChatRequest):
|
| 582 |
-
"""General chat endpoint for partner simulation"""
|
| 583 |
if not ANTHROPIC_API_KEY:
|
| 584 |
raise HTTPException(status_code=500, detail="API key not configured")
|
| 585 |
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
},
|
| 595 |
-
json={
|
| 596 |
-
"model": "claude-sonnet-4-20250514",
|
| 597 |
-
"max_tokens": request.max_tokens,
|
| 598 |
-
"temperature": 0.5,
|
| 599 |
-
"system": request.system,
|
| 600 |
-
"messages": [{"role": m.role, "content": m.content} for m in request.messages]
|
| 601 |
-
},
|
| 602 |
-
timeout=60.0
|
| 603 |
-
)
|
| 604 |
-
response.raise_for_status()
|
| 605 |
-
return response.json()
|
| 606 |
-
except httpx.HTTPStatusError as e:
|
| 607 |
-
raise HTTPException(status_code=e.response.status_code, detail=str(e))
|
| 608 |
-
except Exception as e:
|
| 609 |
-
raise HTTPException(status_code=500, detail=str(e))
|
| 610 |
|
| 611 |
|
| 612 |
@app.get("/api/health")
|
|
|
|
| 8 |
Created by Jocelyn Skillman, LMHC
|
| 9 |
"""
|
| 10 |
|
| 11 |
+
import json
|
| 12 |
import os
|
| 13 |
+
from contextlib import asynccontextmanager
|
| 14 |
+
|
| 15 |
import httpx
|
| 16 |
from fastapi import FastAPI, HTTPException
|
| 17 |
from fastapi.staticfiles import StaticFiles
|
| 18 |
+
from fastapi.responses import FileResponse, StreamingResponse
|
| 19 |
from pydantic import BaseModel
|
| 20 |
from typing import List, Optional
|
| 21 |
|
| 22 |
+
# Reused across requests so calls to api.anthropic.com share a warm
|
| 23 |
+
# connection pool instead of paying a fresh TCP+TLS handshake every turn.
|
| 24 |
+
http_client: httpx.AsyncClient | None = None
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
@asynccontextmanager
|
| 28 |
+
async def lifespan(app: FastAPI):
|
| 29 |
+
global http_client
|
| 30 |
+
http_client = httpx.AsyncClient(timeout=60.0)
|
| 31 |
+
yield
|
| 32 |
+
await http_client.aclose()
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
app = FastAPI(title="Tend & Send ARI Prototype", lifespan=lifespan)
|
| 36 |
|
| 37 |
# Get API key from environment (set as HF Space secret)
|
| 38 |
# Check multiple possible secret names for flexibility
|
|
|
|
| 520 |
max_tokens: int = 500
|
| 521 |
|
| 522 |
|
| 523 |
+
# ============================================================================
|
| 524 |
+
# STREAMING HELPER
|
| 525 |
+
# ============================================================================
|
| 526 |
+
|
| 527 |
+
async def stream_anthropic_text(payload: dict):
|
| 528 |
+
"""POST to the Anthropic Messages API with streaming enabled and yield
|
| 529 |
+
plain-text chunks as they arrive, so the browser can render the reply
|
| 530 |
+
incrementally instead of waiting for the full completion."""
|
| 531 |
+
payload = {**payload, "stream": True}
|
| 532 |
+
async with http_client.stream(
|
| 533 |
+
"POST",
|
| 534 |
+
"https://api.anthropic.com/v1/messages",
|
| 535 |
+
headers={
|
| 536 |
+
"Content-Type": "application/json",
|
| 537 |
+
"x-api-key": ANTHROPIC_API_KEY,
|
| 538 |
+
"anthropic-version": "2023-06-01"
|
| 539 |
+
},
|
| 540 |
+
json=payload
|
| 541 |
+
) as response:
|
| 542 |
+
if response.status_code >= 400:
|
| 543 |
+
body = await response.aread()
|
| 544 |
+
raise HTTPException(status_code=response.status_code, detail=body.decode(errors="replace"))
|
| 545 |
+
|
| 546 |
+
async for line in response.aiter_lines():
|
| 547 |
+
if not line.startswith("data: "):
|
| 548 |
+
continue
|
| 549 |
+
data = line[len("data: "):]
|
| 550 |
+
try:
|
| 551 |
+
event = json.loads(data)
|
| 552 |
+
except json.JSONDecodeError:
|
| 553 |
+
continue
|
| 554 |
+
if event.get("type") == "content_block_delta":
|
| 555 |
+
delta = event.get("delta", {})
|
| 556 |
+
text = delta.get("text")
|
| 557 |
+
if text:
|
| 558 |
+
yield text
|
| 559 |
+
|
| 560 |
+
|
| 561 |
# ============================================================================
|
| 562 |
# API ENDPOINTS
|
| 563 |
# ============================================================================
|
| 564 |
|
| 565 |
@app.post("/api/tool")
|
| 566 |
async def use_tool(request: ToolRequest):
|
| 567 |
+
"""Process an ARI tool request, streaming the reply as plain text."""
|
| 568 |
if not ANTHROPIC_API_KEY:
|
| 569 |
raise HTTPException(status_code=500, detail="API key not configured")
|
| 570 |
|
|
|
|
| 601 |
user_message += f"Current stage: {request.stage}\n\n"
|
| 602 |
user_message += request.user_input
|
| 603 |
|
| 604 |
+
payload = {
|
| 605 |
+
"model": "claude-sonnet-4-20250514",
|
| 606 |
+
"max_tokens": 1000,
|
| 607 |
+
"temperature": 0.4,
|
| 608 |
+
"system": system_prompt,
|
| 609 |
+
"messages": [{"role": "user", "content": user_message}]
|
| 610 |
+
}
|
| 611 |
+
return StreamingResponse(stream_anthropic_text(payload), media_type="text/plain; charset=utf-8")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 612 |
|
| 613 |
|
| 614 |
@app.post("/api/chat")
|
| 615 |
async def chat(request: ChatRequest):
|
| 616 |
+
"""General chat endpoint for partner simulation, streamed as plain text."""
|
| 617 |
if not ANTHROPIC_API_KEY:
|
| 618 |
raise HTTPException(status_code=500, detail="API key not configured")
|
| 619 |
|
| 620 |
+
payload = {
|
| 621 |
+
"model": "claude-sonnet-4-20250514",
|
| 622 |
+
"max_tokens": request.max_tokens,
|
| 623 |
+
"temperature": 0.5,
|
| 624 |
+
"system": request.system,
|
| 625 |
+
"messages": [{"role": m.role, "content": m.content} for m in request.messages]
|
| 626 |
+
}
|
| 627 |
+
return StreamingResponse(stream_anthropic_text(payload), media_type="text/plain; charset=utf-8")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 628 |
|
| 629 |
|
| 630 |
@app.get("/api/health")
|
static/index.html
CHANGED
|
@@ -1126,6 +1126,35 @@
|
|
| 1126 |
let lastPartnerMessage = '';
|
| 1127 |
let conversationStarted = false;
|
| 1128 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1129 |
const TOOL_MAP = {
|
| 1130 |
tend: { name: 'TEND Transform', icon: '✨', api: 'tend' },
|
| 1131 |
soma: { name: 'Somatic Check-in', icon: '🫁', api: 'somatic_checkin' },
|
|
@@ -1573,30 +1602,30 @@ Real grief, real limits. 1-2 sentences.`
|
|
| 1573 |
addToolLoading();
|
| 1574 |
const isVerbose = document.getElementById('toggle-verbose')?.checked || false;
|
| 1575 |
|
|
|
|
| 1576 |
try {
|
| 1577 |
-
const
|
| 1578 |
-
|
| 1579 |
-
|
| 1580 |
-
|
| 1581 |
-
|
| 1582 |
-
|
| 1583 |
-
|
| 1584 |
-
|
| 1585 |
-
|
| 1586 |
-
|
|
|
|
|
|
|
| 1587 |
});
|
| 1588 |
|
| 1589 |
-
|
| 1590 |
-
|
| 1591 |
-
|
| 1592 |
-
if (data.response) {
|
| 1593 |
-
addToolMsg('tool', data.response, TOOL_MAP[tool]?.name);
|
| 1594 |
-
} else {
|
| 1595 |
addToolMsg('tool', 'Could not process. Check API.', 'Error');
|
| 1596 |
}
|
| 1597 |
} catch (error) {
|
| 1598 |
removeToolLoading();
|
| 1599 |
-
|
|
|
|
| 1600 |
}
|
| 1601 |
}
|
| 1602 |
|
|
@@ -1611,32 +1640,31 @@ Real grief, real limits. 1-2 sentences.`
|
|
| 1611 |
|
| 1612 |
addToolLoading();
|
| 1613 |
const isVerbose = document.getElementById('toggle-verbose')?.checked || false;
|
|
|
|
| 1614 |
try {
|
| 1615 |
-
|
| 1616 |
-
|
| 1617 |
-
|
| 1618 |
-
|
| 1619 |
-
|
| 1620 |
-
partner_message: lastPartnerMessage,
|
| 1621 |
-
user_draft: '',
|
| 1622 |
-
user_input: `The person identified these feelings: ${feelingsText}. ${userInput ? 'They added: ' + userInput : ''}
|
| 1623 |
|
| 1624 |
Be BRIEF (3-5 sentences max). No headers, no bullet points, no explicit citations.
|
| 1625 |
|
| 1626 |
Validate the feeling. Share one insight about what this feeling tends to signal in relationships or the nervous system. Name what need might be underneath. Keep it warm and conversational - building emotional intelligence, not lecturing.
|
| 1627 |
|
| 1628 |
Draw wisdom from attachment neuroscience, somatic awareness, and relational psychology - but embed it naturally, don't name sources.`,
|
| 1629 |
-
|
| 1630 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1631 |
});
|
| 1632 |
-
const data = await response.json();
|
| 1633 |
-
removeToolLoading();
|
| 1634 |
-
if (data.response) {
|
| 1635 |
-
addToolMsg('tool', data.response, 'Feelings');
|
| 1636 |
-
}
|
| 1637 |
} catch (e) {
|
| 1638 |
removeToolLoading();
|
| 1639 |
-
|
|
|
|
| 1640 |
}
|
| 1641 |
input.value = '';
|
| 1642 |
return;
|
|
@@ -1649,32 +1677,31 @@ Draw wisdom from attachment neuroscience, somatic awareness, and relational psyc
|
|
| 1649 |
|
| 1650 |
addToolLoading();
|
| 1651 |
const isVerbose = document.getElementById('toggle-verbose')?.checked || false;
|
|
|
|
| 1652 |
try {
|
| 1653 |
-
|
| 1654 |
-
|
| 1655 |
-
|
| 1656 |
-
|
| 1657 |
-
|
| 1658 |
-
partner_message: lastPartnerMessage,
|
| 1659 |
-
user_draft: '',
|
| 1660 |
-
user_input: `The person identified these needs: ${needsText}. ${userInput ? 'They added: ' + userInput : ''}
|
| 1661 |
|
| 1662 |
Be BRIEF (3-5 sentences max). No headers, no bullet points, no explicit citations.
|
| 1663 |
|
| 1664 |
Validate these as universal human needs. Share one insight about why these needs matter in connection. Optionally suggest what a request might sound like. Keep it warm and conversational - building emotional intelligence, not lecturing.
|
| 1665 |
|
| 1666 |
Draw wisdom from NVC, attachment theory, and relational psychology - but embed it naturally, don't name sources.`,
|
| 1667 |
-
|
| 1668 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1669 |
});
|
| 1670 |
-
const data = await response.json();
|
| 1671 |
-
removeToolLoading();
|
| 1672 |
-
if (data.response) {
|
| 1673 |
-
addToolMsg('tool', data.response, 'Needs');
|
| 1674 |
-
}
|
| 1675 |
} catch (e) {
|
| 1676 |
removeToolLoading();
|
| 1677 |
-
|
|
|
|
| 1678 |
}
|
| 1679 |
input.value = '';
|
| 1680 |
return;
|
|
@@ -1700,15 +1727,13 @@ Draw wisdom from NVC, attachment theory, and relational psychology - but embed i
|
|
| 1700 |
|
| 1701 |
addToolLoading();
|
| 1702 |
const isVerbose = document.getElementById('toggle-verbose')?.checked || false;
|
|
|
|
| 1703 |
try {
|
| 1704 |
-
|
| 1705 |
-
|
| 1706 |
-
|
| 1707 |
-
|
| 1708 |
-
|
| 1709 |
-
partner_message: lastPartnerMessage,
|
| 1710 |
-
user_draft: userInput || '',
|
| 1711 |
-
user_input: `The user draws wisdom from: ${selectedTradition}.
|
| 1712 |
|
| 1713 |
Context:
|
| 1714 |
${contextText}
|
|
@@ -1716,16 +1741,17 @@ ${contextText}
|
|
| 1716 |
${userInput ? 'User adds: ' + userInput : ''}
|
| 1717 |
|
| 1718 |
Based on the themes here (conflict, connection, fear, love, boundaries, etc.), provide ONE relevant quote from ${selectedTradition} that speaks to this situation.`,
|
| 1719 |
-
|
| 1720 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1721 |
});
|
| 1722 |
-
const data = await response.json();
|
| 1723 |
-
removeToolLoading();
|
| 1724 |
-
if (data.response) {
|
| 1725 |
-
addToolMsg('tool', data.response, `${selectedTradition} Wisdom`);
|
| 1726 |
-
}
|
| 1727 |
} catch (e) {
|
| 1728 |
removeToolLoading();
|
|
|
|
| 1729 |
addToolMsg('tool', 'Connection error.', 'Error');
|
| 1730 |
}
|
| 1731 |
input.value = '';
|
|
@@ -1820,28 +1846,27 @@ Based on the themes here (conflict, connection, fear, love, boundaries, etc.), p
|
|
| 1820 |
// Partner starts
|
| 1821 |
if (context) {
|
| 1822 |
addLoading();
|
|
|
|
| 1823 |
try {
|
| 1824 |
-
const
|
| 1825 |
-
|
| 1826 |
-
|
| 1827 |
-
|
| 1828 |
-
|
| 1829 |
-
|
| 1830 |
-
|
| 1831 |
-
|
|
|
|
|
|
|
| 1832 |
});
|
| 1833 |
-
|
| 1834 |
-
|
| 1835 |
-
|
| 1836 |
-
const msg = data.content[0].text;
|
| 1837 |
-
addMessage('partner', msg);
|
| 1838 |
-
lastPartnerMessage = msg;
|
| 1839 |
-
conversationHistory.push({ role: 'assistant', content: msg });
|
| 1840 |
}
|
| 1841 |
} catch (e) {
|
| 1842 |
removeLoading();
|
| 1843 |
const msg = OPENING_MESSAGES[style][difficulty];
|
| 1844 |
-
addMessage('partner', msg);
|
| 1845 |
lastPartnerMessage = msg;
|
| 1846 |
conversationHistory.push({ role: 'assistant', content: msg });
|
| 1847 |
}
|
|
@@ -1878,28 +1903,26 @@ Based on the themes here (conflict, connection, fear, love, boundaries, etc.), p
|
|
| 1878 |
conversationHistory.push({ role: 'user', content: message });
|
| 1879 |
|
| 1880 |
addLoading();
|
|
|
|
| 1881 |
try {
|
| 1882 |
const style = document.getElementById('attachment-style').value;
|
| 1883 |
const difficulty = document.getElementById('difficulty').value;
|
| 1884 |
|
| 1885 |
-
const
|
| 1886 |
-
|
| 1887 |
-
|
| 1888 |
-
|
| 1889 |
-
|
| 1890 |
-
|
| 1891 |
-
|
| 1892 |
-
|
|
|
|
|
|
|
| 1893 |
});
|
| 1894 |
|
| 1895 |
-
|
| 1896 |
-
|
| 1897 |
-
|
| 1898 |
-
if (data.content?.[0]) {
|
| 1899 |
-
const partnerMsg = data.content[0].text;
|
| 1900 |
-
addMessage('partner', partnerMsg);
|
| 1901 |
-
lastPartnerMessage = partnerMsg;
|
| 1902 |
-
conversationHistory.push({ role: 'assistant', content: partnerMsg });
|
| 1903 |
}
|
| 1904 |
} catch (error) {
|
| 1905 |
removeLoading();
|
|
@@ -1961,6 +1984,7 @@ Based on the themes here (conflict, connection, fear, love, boundaries, etc.), p
|
|
| 1961 |
|
| 1962 |
conv.appendChild(div);
|
| 1963 |
conv.scrollTop = conv.scrollHeight;
|
|
|
|
| 1964 |
}
|
| 1965 |
|
| 1966 |
function addToolMsg(type, content, label = null) {
|
|
@@ -1979,6 +2003,7 @@ Based on the themes here (conflict, connection, fear, love, boundaries, etc.), p
|
|
| 1979 |
|
| 1980 |
cont.appendChild(div);
|
| 1981 |
cont.scrollTop = cont.scrollHeight;
|
|
|
|
| 1982 |
}
|
| 1983 |
|
| 1984 |
function addLoading() {
|
|
|
|
| 1126 |
let lastPartnerMessage = '';
|
| 1127 |
let conversationStarted = false;
|
| 1128 |
|
| 1129 |
+
// Streams a POST response's body as it arrives, calling onChunk with
|
| 1130 |
+
// each piece of text, and resolves with the full accumulated text.
|
| 1131 |
+
// Falls back to reading the whole body at once on error responses.
|
| 1132 |
+
async function streamFetch(url, body, onChunk) {
|
| 1133 |
+
const response = await fetch(url, {
|
| 1134 |
+
method: 'POST',
|
| 1135 |
+
headers: { 'Content-Type': 'application/json' },
|
| 1136 |
+
body: JSON.stringify(body)
|
| 1137 |
+
});
|
| 1138 |
+
|
| 1139 |
+
if (!response.ok) {
|
| 1140 |
+
let detail = 'Request failed.';
|
| 1141 |
+
try { detail = (await response.json()).detail || detail; } catch (e) {}
|
| 1142 |
+
throw new Error(detail);
|
| 1143 |
+
}
|
| 1144 |
+
|
| 1145 |
+
const reader = response.body.getReader();
|
| 1146 |
+
const decoder = new TextDecoder();
|
| 1147 |
+
let full = '';
|
| 1148 |
+
while (true) {
|
| 1149 |
+
const { done, value } = await reader.read();
|
| 1150 |
+
if (done) break;
|
| 1151 |
+
const chunk = decoder.decode(value, { stream: true });
|
| 1152 |
+
full += chunk;
|
| 1153 |
+
onChunk(chunk, full);
|
| 1154 |
+
}
|
| 1155 |
+
return full;
|
| 1156 |
+
}
|
| 1157 |
+
|
| 1158 |
const TOOL_MAP = {
|
| 1159 |
tend: { name: 'TEND Transform', icon: '✨', api: 'tend' },
|
| 1160 |
soma: { name: 'Somatic Check-in', icon: '🫁', api: 'somatic_checkin' },
|
|
|
|
| 1602 |
addToolLoading();
|
| 1603 |
const isVerbose = document.getElementById('toggle-verbose')?.checked || false;
|
| 1604 |
|
| 1605 |
+
let textEl = null;
|
| 1606 |
try {
|
| 1607 |
+
const full = await streamFetch('/api/tool', {
|
| 1608 |
+
tool: apiTool,
|
| 1609 |
+
partner_message: lastPartnerMessage,
|
| 1610 |
+
user_draft: userInput,
|
| 1611 |
+
user_input: inputText,
|
| 1612 |
+
verbose: isVerbose
|
| 1613 |
+
}, (chunk, acc) => {
|
| 1614 |
+
if (!textEl) {
|
| 1615 |
+
removeToolLoading();
|
| 1616 |
+
textEl = addToolMsg('tool', '', TOOL_MAP[tool]?.name);
|
| 1617 |
+
}
|
| 1618 |
+
textEl.innerHTML = formatText(acc);
|
| 1619 |
});
|
| 1620 |
|
| 1621 |
+
if (!full) {
|
| 1622 |
+
removeToolLoading();
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1623 |
addToolMsg('tool', 'Could not process. Check API.', 'Error');
|
| 1624 |
}
|
| 1625 |
} catch (error) {
|
| 1626 |
removeToolLoading();
|
| 1627 |
+
if (textEl) { textEl.innerHTML = formatText('Connection error.'); }
|
| 1628 |
+
else { addToolMsg('tool', 'Connection error.', 'Error'); }
|
| 1629 |
}
|
| 1630 |
}
|
| 1631 |
|
|
|
|
| 1640 |
|
| 1641 |
addToolLoading();
|
| 1642 |
const isVerbose = document.getElementById('toggle-verbose')?.checked || false;
|
| 1643 |
+
let textEl = null;
|
| 1644 |
try {
|
| 1645 |
+
await streamFetch('/api/tool', {
|
| 1646 |
+
tool: 'feelings_needs',
|
| 1647 |
+
partner_message: lastPartnerMessage,
|
| 1648 |
+
user_draft: '',
|
| 1649 |
+
user_input: `The person identified these feelings: ${feelingsText}. ${userInput ? 'They added: ' + userInput : ''}
|
|
|
|
|
|
|
|
|
|
| 1650 |
|
| 1651 |
Be BRIEF (3-5 sentences max). No headers, no bullet points, no explicit citations.
|
| 1652 |
|
| 1653 |
Validate the feeling. Share one insight about what this feeling tends to signal in relationships or the nervous system. Name what need might be underneath. Keep it warm and conversational - building emotional intelligence, not lecturing.
|
| 1654 |
|
| 1655 |
Draw wisdom from attachment neuroscience, somatic awareness, and relational psychology - but embed it naturally, don't name sources.`,
|
| 1656 |
+
verbose: isVerbose
|
| 1657 |
+
}, (chunk, acc) => {
|
| 1658 |
+
if (!textEl) {
|
| 1659 |
+
removeToolLoading();
|
| 1660 |
+
textEl = addToolMsg('tool', '', 'Feelings');
|
| 1661 |
+
}
|
| 1662 |
+
textEl.innerHTML = formatText(acc);
|
| 1663 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1664 |
} catch (e) {
|
| 1665 |
removeToolLoading();
|
| 1666 |
+
if (textEl) { textEl.innerHTML = formatText('Connection error.'); }
|
| 1667 |
+
else { addToolMsg('tool', 'Connection error.', 'Error'); }
|
| 1668 |
}
|
| 1669 |
input.value = '';
|
| 1670 |
return;
|
|
|
|
| 1677 |
|
| 1678 |
addToolLoading();
|
| 1679 |
const isVerbose = document.getElementById('toggle-verbose')?.checked || false;
|
| 1680 |
+
let textEl = null;
|
| 1681 |
try {
|
| 1682 |
+
await streamFetch('/api/tool', {
|
| 1683 |
+
tool: 'feelings_needs',
|
| 1684 |
+
partner_message: lastPartnerMessage,
|
| 1685 |
+
user_draft: '',
|
| 1686 |
+
user_input: `The person identified these needs: ${needsText}. ${userInput ? 'They added: ' + userInput : ''}
|
|
|
|
|
|
|
|
|
|
| 1687 |
|
| 1688 |
Be BRIEF (3-5 sentences max). No headers, no bullet points, no explicit citations.
|
| 1689 |
|
| 1690 |
Validate these as universal human needs. Share one insight about why these needs matter in connection. Optionally suggest what a request might sound like. Keep it warm and conversational - building emotional intelligence, not lecturing.
|
| 1691 |
|
| 1692 |
Draw wisdom from NVC, attachment theory, and relational psychology - but embed it naturally, don't name sources.`,
|
| 1693 |
+
verbose: isVerbose
|
| 1694 |
+
}, (chunk, acc) => {
|
| 1695 |
+
if (!textEl) {
|
| 1696 |
+
removeToolLoading();
|
| 1697 |
+
textEl = addToolMsg('tool', '', 'Needs');
|
| 1698 |
+
}
|
| 1699 |
+
textEl.innerHTML = formatText(acc);
|
| 1700 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1701 |
} catch (e) {
|
| 1702 |
removeToolLoading();
|
| 1703 |
+
if (textEl) { textEl.innerHTML = formatText('Connection error.'); }
|
| 1704 |
+
else { addToolMsg('tool', 'Connection error.', 'Error'); }
|
| 1705 |
}
|
| 1706 |
input.value = '';
|
| 1707 |
return;
|
|
|
|
| 1727 |
|
| 1728 |
addToolLoading();
|
| 1729 |
const isVerbose = document.getElementById('toggle-verbose')?.checked || false;
|
| 1730 |
+
let textEl = null;
|
| 1731 |
try {
|
| 1732 |
+
await streamFetch('/api/tool', {
|
| 1733 |
+
tool: 'wisdom',
|
| 1734 |
+
partner_message: lastPartnerMessage,
|
| 1735 |
+
user_draft: userInput || '',
|
| 1736 |
+
user_input: `The user draws wisdom from: ${selectedTradition}.
|
|
|
|
|
|
|
|
|
|
| 1737 |
|
| 1738 |
Context:
|
| 1739 |
${contextText}
|
|
|
|
| 1741 |
${userInput ? 'User adds: ' + userInput : ''}
|
| 1742 |
|
| 1743 |
Based on the themes here (conflict, connection, fear, love, boundaries, etc.), provide ONE relevant quote from ${selectedTradition} that speaks to this situation.`,
|
| 1744 |
+
verbose: isVerbose
|
| 1745 |
+
}, (chunk, acc) => {
|
| 1746 |
+
if (!textEl) {
|
| 1747 |
+
removeToolLoading();
|
| 1748 |
+
textEl = addToolMsg('tool', '', `${selectedTradition} Wisdom`);
|
| 1749 |
+
}
|
| 1750 |
+
textEl.innerHTML = formatText(acc);
|
| 1751 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1752 |
} catch (e) {
|
| 1753 |
removeToolLoading();
|
| 1754 |
+
if (textEl) { textEl.innerHTML = formatText('Connection error.'); return; }
|
| 1755 |
addToolMsg('tool', 'Connection error.', 'Error');
|
| 1756 |
}
|
| 1757 |
input.value = '';
|
|
|
|
| 1846 |
// Partner starts
|
| 1847 |
if (context) {
|
| 1848 |
addLoading();
|
| 1849 |
+
let textEl = null;
|
| 1850 |
try {
|
| 1851 |
+
const full = await streamFetch('/api/chat', {
|
| 1852 |
+
messages: [{ role: 'user', content: 'Start based on: ' + context }],
|
| 1853 |
+
system: PARTNER_PROMPTS[style][difficulty] + ' CONTEXT: ' + context,
|
| 1854 |
+
max_tokens: 200
|
| 1855 |
+
}, (chunk, acc) => {
|
| 1856 |
+
if (!textEl) {
|
| 1857 |
+
removeLoading();
|
| 1858 |
+
textEl = addMessage('partner', '');
|
| 1859 |
+
}
|
| 1860 |
+
textEl.innerHTML = formatText(acc);
|
| 1861 |
});
|
| 1862 |
+
if (full) {
|
| 1863 |
+
lastPartnerMessage = full;
|
| 1864 |
+
conversationHistory.push({ role: 'assistant', content: full });
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1865 |
}
|
| 1866 |
} catch (e) {
|
| 1867 |
removeLoading();
|
| 1868 |
const msg = OPENING_MESSAGES[style][difficulty];
|
| 1869 |
+
if (textEl) { textEl.innerHTML = formatText(msg); } else { addMessage('partner', msg); }
|
| 1870 |
lastPartnerMessage = msg;
|
| 1871 |
conversationHistory.push({ role: 'assistant', content: msg });
|
| 1872 |
}
|
|
|
|
| 1903 |
conversationHistory.push({ role: 'user', content: message });
|
| 1904 |
|
| 1905 |
addLoading();
|
| 1906 |
+
let textEl = null;
|
| 1907 |
try {
|
| 1908 |
const style = document.getElementById('attachment-style').value;
|
| 1909 |
const difficulty = document.getElementById('difficulty').value;
|
| 1910 |
|
| 1911 |
+
const full = await streamFetch('/api/chat', {
|
| 1912 |
+
messages: conversationHistory,
|
| 1913 |
+
system: PARTNER_PROMPTS[style][difficulty],
|
| 1914 |
+
max_tokens: 300
|
| 1915 |
+
}, (chunk, acc) => {
|
| 1916 |
+
if (!textEl) {
|
| 1917 |
+
removeLoading();
|
| 1918 |
+
textEl = addMessage('partner', '');
|
| 1919 |
+
}
|
| 1920 |
+
textEl.innerHTML = formatText(acc);
|
| 1921 |
});
|
| 1922 |
|
| 1923 |
+
if (full) {
|
| 1924 |
+
lastPartnerMessage = full;
|
| 1925 |
+
conversationHistory.push({ role: 'assistant', content: full });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1926 |
}
|
| 1927 |
} catch (error) {
|
| 1928 |
removeLoading();
|
|
|
|
| 1984 |
|
| 1985 |
conv.appendChild(div);
|
| 1986 |
conv.scrollTop = conv.scrollHeight;
|
| 1987 |
+
return text;
|
| 1988 |
}
|
| 1989 |
|
| 1990 |
function addToolMsg(type, content, label = null) {
|
|
|
|
| 2003 |
|
| 2004 |
cont.appendChild(div);
|
| 2005 |
cont.scrollTop = cont.scrollHeight;
|
| 2006 |
+
return text;
|
| 2007 |
}
|
| 2008 |
|
| 2009 |
function addLoading() {
|