Spaces:
Sleeping
Sleeping
Commit Β·
a267a62
1
Parent(s): c35deb5
added questionnaire
Browse files- README.md +18 -6
- backend/app.py +8 -12
- backend/graph.py +332 -197
- backend/prompts.py +122 -191
- frontend/app.js +1 -1
- frontend/index.html +12 -16
README.md
CHANGED
|
@@ -7,12 +7,24 @@ sdk: docker
|
|
| 7 |
pinned: false
|
| 8 |
---
|
| 9 |
|
| 10 |
-
#
|
| 11 |
|
| 12 |
-
A
|
| 13 |
|
| 14 |
## Features
|
| 15 |
-
|
| 16 |
-
-
|
| 17 |
-
-
|
| 18 |
-
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
pinned: false
|
| 8 |
---
|
| 9 |
|
| 10 |
+
# Stacklogix Consultant Chatbot
|
| 11 |
|
| 12 |
+
A consultant-style chatbot for **Stacklogix** that gathers client requirements and explains how Stacklogix will address their needs. Powered by LangGraph + Groq (Llama 3.3 70B). Designed to be embedded on the Stacklogix website where clients interact with it instead of a sales representative.
|
| 13 |
|
| 14 |
## Features
|
| 15 |
+
|
| 16 |
+
- **Dual mode**: Handles both **clear requests** (e.g. build a chatbot, build a dashboard) and **problems or vague situations** (e.g. inventory moving slowly, issues with an existing project).
|
| 17 |
+
- **Structured requirement collection**: Asks relevant questions (e.g. type of deliverable, data and format, behavior, look and feel, constraints, expectations) in a natural, consultant-like flow.
|
| 18 |
+
- **Consultant-style explanations**: Explains how the problem will be addressed, high-level implementation or solution steps, and what the company will do to deliver or resolve the issue (and, for problems, prevent recurrence).
|
| 19 |
+
- **Domain-agnostic**: Works across technology, operations, business processes, inventory, and any other domain.
|
| 20 |
+
- **5-phase flow**: Discovery β Exploration β Constraints β Solution β Refinement, with session management and SQLite persistence.
|
| 21 |
+
|
| 22 |
+
## Flow
|
| 23 |
+
|
| 24 |
+
1. **Discovery** β Understand what the client wants or what is going wrong.
|
| 25 |
+
2. **Exploration** β Gather requirements: data, format, behavior, look, constraints, success criteria.
|
| 26 |
+
3. **Constraints** β Confirm understanding and explain the proposed approach.
|
| 27 |
+
4. **Solution** β Summarize needs, how you will address it, steps, and what you will deliver.
|
| 28 |
+
5. **Refinement** β Full proposal, implementation steps, and next steps.
|
| 29 |
+
|
| 30 |
+
Clients can start with a clear request (βwe want to build Xβ) or a problem (βY isnβt workingβ); the chatbot adapts its questions and explanations accordingly.
|
backend/app.py
CHANGED
|
@@ -40,15 +40,11 @@ from graph import run_consultation
|
|
| 40 |
# ---------------------------------------------------------------------------
|
| 41 |
|
| 42 |
GREETING_MESSAGE = (
|
| 43 |
-
"**Welcome
|
| 44 |
-
"
|
| 45 |
-
"
|
| 46 |
-
"
|
| 47 |
-
"
|
| 48 |
-
"- You describe your challenge or goal\n"
|
| 49 |
-
"- I'll ask smart, targeted questions β and share insights along the way\n"
|
| 50 |
-
"- Together, we'll converge on a clear, actionable solution\n\n"
|
| 51 |
-
"**So β what's on your mind? What problem are you trying to solve?**"
|
| 52 |
)
|
| 53 |
|
| 54 |
|
|
@@ -63,8 +59,8 @@ async def lifespan(app: FastAPI):
|
|
| 63 |
|
| 64 |
|
| 65 |
app = FastAPI(
|
| 66 |
-
title="Consultant Chatbot API",
|
| 67 |
-
description="
|
| 68 |
version="1.0.0",
|
| 69 |
lifespan=lifespan,
|
| 70 |
)
|
|
@@ -155,7 +151,7 @@ async def remove_session(session_id: str):
|
|
| 155 |
|
| 156 |
@app.get("/health")
|
| 157 |
async def health():
|
| 158 |
-
return {"status": "healthy", "service": "
|
| 159 |
|
| 160 |
|
| 161 |
# ---------------------------------------------------------------------------
|
|
|
|
| 40 |
# ---------------------------------------------------------------------------
|
| 41 |
|
| 42 |
GREETING_MESSAGE = (
|
| 43 |
+
"**Welcome to Stacklogix.** I'm your consultant for this session. π\n\n"
|
| 44 |
+
"Whether you have a **clear request** (e.g. build a product or fix an issue) or a **problem** "
|
| 45 |
+
"(e.g. something isn't working as you'd like), I'll ask a few questions to understand your needs. "
|
| 46 |
+
"Our team will then follow up with you.\n\n"
|
| 47 |
+
"**What would you like to achieve or what do you need help with?**"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
)
|
| 49 |
|
| 50 |
|
|
|
|
| 59 |
|
| 60 |
|
| 61 |
app = FastAPI(
|
| 62 |
+
title="Stacklogix Consultant Chatbot API",
|
| 63 |
+
description="Stacklogix requirement-collection chatbot β gathers client requirements and explains how Stacklogix will address them. LangGraph + Groq.",
|
| 64 |
version="1.0.0",
|
| 65 |
lifespan=lifespan,
|
| 66 |
)
|
|
|
|
| 151 |
|
| 152 |
@app.get("/health")
|
| 153 |
async def health():
|
| 154 |
+
return {"status": "healthy", "service": "stacklogix-chatbot"}
|
| 155 |
|
| 156 |
|
| 157 |
# ---------------------------------------------------------------------------
|
backend/graph.py
CHANGED
|
@@ -1,14 +1,14 @@
|
|
| 1 |
-
"""LangGraph-based consultation state machine.
|
| 2 |
|
| 3 |
-
Implements a 5-phase
|
|
|
|
| 4 |
|
| 5 |
CODE-LEVEL QUALITY ENFORCEMENT:
|
| 6 |
-
-
|
| 7 |
-
- Anti-repetition
|
| 8 |
-
- Anti-hallucination
|
| 9 |
-
- Deferral counter:
|
| 10 |
-
- Turn-aware instructions
|
| 11 |
-
- Understanding validation: ensures the LLM doesn't fabricate facts
|
| 12 |
"""
|
| 13 |
|
| 14 |
from __future__ import annotations
|
|
@@ -22,7 +22,28 @@ from langchain_core.messages import HumanMessage, SystemMessage
|
|
| 22 |
|
| 23 |
from llm import llm
|
| 24 |
from models import Message, Phase, SessionState, PHASE_ORDER, PHASE_THRESHOLDS
|
| 25 |
-
from prompts import PHASE_PROMPTS
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
|
| 27 |
|
| 28 |
# ---------------------------------------------------------------------------
|
|
@@ -39,79 +60,20 @@ class GraphState(TypedDict):
|
|
| 39 |
|
| 40 |
|
| 41 |
# ---------------------------------------------------------------------------
|
| 42 |
-
#
|
| 43 |
# ---------------------------------------------------------------------------
|
| 44 |
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
"
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
"
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
"label": "Problem Details",
|
| 57 |
-
"description": "What exactly is going wrong, since when, how bad",
|
| 58 |
-
"keywords": ["problem", "issue", "challenge", "slow", "decline",
|
| 59 |
-
"month", "year", "since", "affect", "impact",
|
| 60 |
-
"drop", "worse", "bad", "struggle", "difficult"],
|
| 61 |
-
"prompt": "You could ask more about the problem β what exactly is happening, how long, how bad.",
|
| 62 |
-
},
|
| 63 |
-
"data_and_attempts": {
|
| 64 |
-
"label": "Data & Past Attempts",
|
| 65 |
-
"description": "Where their data lives and what they've tried already",
|
| 66 |
-
"keywords": ["data", "excel", "spreadsheet", "track", "record",
|
| 67 |
-
"tried", "attempt", "before", "already", "didn't work",
|
| 68 |
-
"keep", "stored", "written", "noted", "logged"],
|
| 69 |
-
"prompt": "IMPORTANT: You haven't asked where they keep their data yet. Ask how they track or record their business info (Excel, notebook, software, etc.).",
|
| 70 |
-
},
|
| 71 |
-
}
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
def _detect_covered_categories(messages: list[Message]) -> dict[str, bool]:
|
| 75 |
-
"""Analyze conversation to determine which question categories have been covered."""
|
| 76 |
-
covered = {cat: False for cat in QUESTION_CATEGORIES}
|
| 77 |
-
|
| 78 |
-
all_text = " ".join(msg.content.lower() for msg in messages)
|
| 79 |
-
|
| 80 |
-
for cat_id, cat_info in QUESTION_CATEGORIES.items():
|
| 81 |
-
keyword_hits = sum(1 for kw in cat_info["keywords"] if kw in all_text)
|
| 82 |
-
# Need at least 2 keyword hits to consider a category covered
|
| 83 |
-
if keyword_hits >= 2:
|
| 84 |
-
covered[cat_id] = True
|
| 85 |
-
|
| 86 |
-
return covered
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
def _build_category_guidance(covered: dict[str, bool], turn_number: int) -> str:
|
| 90 |
-
"""Build a SOFT guidance block β suggestions, not demands."""
|
| 91 |
-
uncovered = [
|
| 92 |
-
QUESTION_CATEGORIES[cat]["prompt"]
|
| 93 |
-
for cat, is_covered in covered.items()
|
| 94 |
-
if not is_covered
|
| 95 |
-
]
|
| 96 |
-
|
| 97 |
-
if not uncovered:
|
| 98 |
-
return "\nYou've gathered enough info. Focus on giving RECOMMENDATIONS now.\n"
|
| 99 |
-
|
| 100 |
-
# Soft suggestion β max 1 at a time to avoid overwhelming
|
| 101 |
-
guidance = "\nπ‘ Suggestion β if relevant, you might want to ask about:\n"
|
| 102 |
-
guidance += f" β {uncovered[0]}\n"
|
| 103 |
-
|
| 104 |
-
covered_list = [
|
| 105 |
-
QUESTION_CATEGORIES[cat]["label"]
|
| 106 |
-
for cat, is_covered in covered.items()
|
| 107 |
-
if is_covered
|
| 108 |
-
]
|
| 109 |
-
if covered_list:
|
| 110 |
-
guidance += f"\nπ« ALREADY COVERED (do NOT ask about these again): {', '.join(covered_list)}\n"
|
| 111 |
-
|
| 112 |
-
guidance += "\nBut ONLY ask if it naturally follows from what the client just said. Do NOT force it.\n"
|
| 113 |
-
|
| 114 |
-
return guidance
|
| 115 |
|
| 116 |
|
| 117 |
# ---------------------------------------------------------------------------
|
|
@@ -184,8 +146,6 @@ def _has_asked_about_data(messages: list[Message]) -> bool:
|
|
| 184 |
for msg in messages:
|
| 185 |
if msg.role == "assistant" and "?" in msg.content:
|
| 186 |
content_lower = msg.content.lower()
|
| 187 |
-
# Check each question sentence specifically
|
| 188 |
-
# Split on ? to get question fragments
|
| 189 |
question_parts = content_lower.split("?")
|
| 190 |
for part in question_parts:
|
| 191 |
for pattern in DATA_STORAGE_QUESTION_PATTERNS:
|
|
@@ -194,6 +154,39 @@ def _has_asked_about_data(messages: list[Message]) -> bool:
|
|
| 194 |
return False
|
| 195 |
|
| 196 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 197 |
def _check_repetition(new_reply: str, previous_replies: list[str]) -> float:
|
| 198 |
"""Check sentence-level overlap with previous replies. Returns 0.0-1.0."""
|
| 199 |
if not previous_replies:
|
|
@@ -235,39 +228,37 @@ def _check_repetition(new_reply: str, previous_replies: list[str]) -> float:
|
|
| 235 |
# Reply quality enforcement (code-level, can't be bypassed by LLM)
|
| 236 |
# ---------------------------------------------------------------------------
|
| 237 |
|
| 238 |
-
#
|
| 239 |
-
|
| 240 |
-
r"
|
| 241 |
-
r"
|
| 242 |
-
r"
|
| 243 |
-
r"
|
| 244 |
-
r"
|
| 245 |
-
r"
|
| 246 |
-
r"
|
| 247 |
-
r"
|
| 248 |
-
r"
|
| 249 |
-
# Catch ANY question with "you think" in it
|
| 250 |
-
r"\?.*you think",
|
| 251 |
-
r"you think.+\?",
|
| 252 |
-
r"(?:reason|cause)\s+you\s+think",
|
| 253 |
-
r"what role (?:do you think |does |)",
|
| 254 |
-
# "What are your thoughts/take/opinion on" patterns
|
| 255 |
-
r"what (?:are |)your (?:thoughts|take|opinion) on",
|
| 256 |
-
r"your thoughts on .+\?",
|
| 257 |
]
|
| 258 |
|
| 259 |
-
#
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
r"
|
| 263 |
-
r"
|
| 264 |
-
r"
|
| 265 |
-
r"
|
| 266 |
-
r"
|
| 267 |
-
r"
|
| 268 |
-
|
| 269 |
-
r"
|
| 270 |
-
r"(?:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 271 |
]
|
| 272 |
|
| 273 |
# Filler SENTENCES that should be stripped from ANYWHERE in the response
|
|
@@ -279,16 +270,15 @@ FILLER_SENTENCES = [
|
|
| 279 |
# "I'd love to" filler
|
| 280 |
r"i'd love to (?:learn|help|explore|understand|hear|know|dig)",
|
| 281 |
r"i'd like to (?:learn|help|explore|understand|hear|know|dig)",
|
| 282 |
-
# Restating the problem robotically
|
| 283 |
-
r"a slow[- ]moving inventory (?:of|can be)",
|
| 284 |
r"(?:can be|is) a (?:challenging|significant|difficult|tough|particularly challenging|common) (?:issue|problem|situation|challenge)",
|
| 285 |
r"(?:this|it) can be a (?:common|challenging|significant) (?:issue|problem|situation)",
|
| 286 |
-
r"especially (?:given|with) the (?:high value|constantly changing|
|
| 287 |
-
r"especially given the high value
|
| 288 |
-
r"(?:
|
| 289 |
-
r"are often purchased for special occasions",
|
| 290 |
-
r"the quality and characteristics of the
|
| 291 |
-
r"the
|
| 292 |
r"it's likely that this is affecting",
|
| 293 |
r"this (?:can|may|might) (?:be|also be) (?:affecting|impacting|taking)",
|
| 294 |
# Robotic transitions
|
|
@@ -308,6 +298,11 @@ FILLER_SENTENCES = [
|
|
| 308 |
r"given the (?:specifics|broad range|unique aspects|diverse nature|complexity)",
|
| 309 |
r"to better understand (?:the scope|your situation|the issue|your)",
|
| 310 |
r"this (?:is a good starting point|indicates you have)",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 311 |
r"i'm excited to learn more",
|
| 312 |
r"i'm looking forward to",
|
| 313 |
r"understanding (?:your|the) specific",
|
|
@@ -336,6 +331,14 @@ FILLER_SENTENCES = [
|
|
| 336 |
r"it(?:'s| is) great that you(?:'ve| have)",
|
| 337 |
# "You've just started our conversation"
|
| 338 |
r"you(?:'ve| have) just started",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 339 |
]
|
| 340 |
|
| 341 |
# Section headers that create robotic formatting
|
|
@@ -347,31 +350,28 @@ RECAP_HEADERS = [
|
|
| 347 |
|
| 348 |
|
| 349 |
def _detect_bad_question_patterns(reply: str) -> str | None:
|
| 350 |
-
"""Detect self-solving
|
| 351 |
|
| 352 |
Returns a correction instruction if bad patterns found, None otherwise.
|
| 353 |
"""
|
| 354 |
reply_lower = reply.lower()
|
| 355 |
|
| 356 |
-
|
|
|
|
| 357 |
if re.search(pattern, reply_lower):
|
| 358 |
return (
|
| 359 |
-
"β οΈ You
|
| 360 |
-
"
|
| 361 |
-
"Instead, ask a SPECIFIC diagnostic question that helps you "
|
| 362 |
-
"narrow down the cause yourself. For example, ask about "
|
| 363 |
-
"specifics of the problem, not what they think is causing it."
|
| 364 |
)
|
| 365 |
-
|
| 366 |
-
for pattern in
|
| 367 |
if re.search(pattern, reply_lower):
|
| 368 |
return (
|
| 369 |
-
"β οΈ
|
| 370 |
-
"
|
| 371 |
-
"Ask a
|
| 372 |
-
"situation without your bias."
|
| 373 |
)
|
| 374 |
-
|
| 375 |
return None
|
| 376 |
|
| 377 |
|
|
@@ -469,6 +469,85 @@ def _break_wall_of_text(reply: str) -> str:
|
|
| 469 |
return "\n\n".join(result)
|
| 470 |
|
| 471 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 472 |
def _format_questions(reply: str) -> str:
|
| 473 |
"""Ensure each question is on its own paragraph with blank lines around it.
|
| 474 |
|
|
@@ -618,12 +697,17 @@ def _build_message_list(state: SessionState, user_message: str) -> list:
|
|
| 618 |
# Keep the conversation history but mark the new topic
|
| 619 |
state.understanding = state.understanding + f"\n\n--- NEW TOPIC: {user_message} ---\n" if state.understanding else ""
|
| 620 |
|
| 621 |
-
# --- HARD PHASE FORCING β
|
| 622 |
-
# Use RELATIVE turn count (turns since current topic started)
|
| 623 |
topic_turns = turn_number - state.topic_start_turn
|
| 624 |
current_phase = state.phase
|
| 625 |
current_confidence = state.confidence
|
| 626 |
-
if
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 627 |
current_phase = Phase.REFINEMENT
|
| 628 |
current_confidence = max(current_confidence, 0.85)
|
| 629 |
state.phase = current_phase
|
|
@@ -640,56 +724,66 @@ def _build_message_list(state: SessionState, user_message: str) -> list:
|
|
| 640 |
phase_prompt = TURN_1_PROMPT
|
| 641 |
else:
|
| 642 |
phase_prompt = PHASE_PROMPTS[current_phase].format(confidence=current_confidence)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 643 |
|
| 644 |
# Add understanding context (validated)
|
| 645 |
if state.understanding:
|
| 646 |
phase_prompt += (
|
| 647 |
-
f"\n\nCLIENT INFO (for YOUR reference only β do NOT
|
| 648 |
f"\n{state.understanding}\n"
|
| 649 |
-
f"
|
| 650 |
-
f"Jump STRAIGHT into new insights, analysis, or questions.\n"
|
| 651 |
)
|
|
|
|
|
|
|
|
|
|
| 652 |
|
| 653 |
# --- CONVERSATION STATS ---
|
| 654 |
stats = f"\nTurn {turn_number} | {questions_asked} questions asked so far | {deferral_count} deferrals\n"
|
|
|
|
|
|
|
|
|
|
| 655 |
phase_prompt += stats
|
| 656 |
|
| 657 |
-
# ---
|
| 658 |
-
|
| 659 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 660 |
phase_prompt += """
|
| 661 |
-
β οΈ
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
covered = _detect_covered_categories(state.messages)
|
| 668 |
-
category_guidance = _build_category_guidance(covered, turn_number)
|
| 669 |
-
phase_prompt += category_guidance
|
| 670 |
|
| 671 |
# --- DYNAMIC BEHAVIORAL OVERRIDES ---
|
| 672 |
if deferral_count >= 2:
|
| 673 |
phase_prompt += """
|
| 674 |
-
β οΈ THE USER SAID "I DON'T KNOW" MULTIPLE TIMES.
|
| 675 |
-
|
| 676 |
-
-
|
| 677 |
-
- LEAD with YOUR specific recommendations: "Based on what you've told me, here's what I'd suggest..."
|
| 678 |
-
- Make reasonable assumptions and state them.
|
| 679 |
-
- Be SPECIFIC: name tools, methods, steps. No generic advice like "review your pricing".
|
| 680 |
-
- You are the EXPERT β the client is paying YOU to figure it out.
|
| 681 |
"""
|
| 682 |
|
| 683 |
# Question count limits only apply during diagnostic phases
|
| 684 |
# In solution/final phases, the prompt itself handles question limits
|
| 685 |
if state.phase in (Phase.DISCOVERY, Phase.EXPLORATION):
|
| 686 |
if questions_asked >= 20:
|
| 687 |
-
phase_prompt += "\nβ οΈ STOP ASKING.
|
| 688 |
elif questions_asked >= 14:
|
| 689 |
-
phase_prompt += "\nYou've asked enough
|
| 690 |
|
| 691 |
if turn_number >= 8:
|
| 692 |
-
phase_prompt += "\nβ οΈ Turn 8+.
|
| 693 |
|
| 694 |
messages = [SystemMessage(content=phase_prompt)]
|
| 695 |
|
|
@@ -794,6 +888,38 @@ def _determine_phase(confidence: float) -> Phase:
|
|
| 794 |
return Phase.DISCOVERY
|
| 795 |
|
| 796 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 797 |
# ---------------------------------------------------------------------------
|
| 798 |
# Main graph execution
|
| 799 |
# ---------------------------------------------------------------------------
|
|
@@ -801,6 +927,19 @@ def _determine_phase(confidence: float) -> Phase:
|
|
| 801 |
async def run_consultation(session_state: SessionState, user_message: str) -> GraphState:
|
| 802 |
"""Run one turn of the consultation with quality enforcement."""
|
| 803 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 804 |
# 1. Build messages with intelligence injection
|
| 805 |
messages = _build_message_list(session_state, user_message)
|
| 806 |
turn_number = _get_turn_number(session_state.messages)
|
|
@@ -809,17 +948,17 @@ async def run_consultation(session_state: SessionState, user_message: str) -> Gr
|
|
| 809 |
response = await llm.ainvoke(messages)
|
| 810 |
parsed = _parse_llm_output(response.content)
|
| 811 |
|
| 812 |
-
# 3. Anti-repetition check
|
| 813 |
previous_replies = _get_previous_bot_responses(session_state.messages)
|
| 814 |
overlap = _check_repetition(parsed["reply"], previous_replies)
|
|
|
|
| 815 |
|
| 816 |
-
if overlap > 0.35 and previous_replies:
|
| 817 |
anti_repeat_msg = SystemMessage(content=(
|
| 818 |
-
"β οΈ
|
| 819 |
-
"
|
| 820 |
-
"-
|
| 821 |
-
"
|
| 822 |
-
"- Ask about a DIFFERENT topic entirely"
|
| 823 |
))
|
| 824 |
messages.insert(-1, anti_repeat_msg)
|
| 825 |
response = await llm.ainvoke(messages)
|
|
@@ -865,36 +1004,21 @@ async def run_consultation(session_state: SessionState, user_message: str) -> Gr
|
|
| 865 |
non_q_lines = [line for line in reply_text.split("\n") if "?" not in line]
|
| 866 |
non_q_content = "\n".join(non_q_lines).strip()
|
| 867 |
|
| 868 |
-
# SOLUTION/REFINEMENT
|
| 869 |
is_final_phase = session_state.phase in (Phase.SOLUTION, Phase.REFINEMENT)
|
| 870 |
-
min_length =
|
| 871 |
|
| 872 |
if len(non_q_content) < min_length:
|
| 873 |
if is_final_phase:
|
| 874 |
expand_msg = SystemMessage(content=(
|
| 875 |
-
"β οΈ
|
| 876 |
-
"
|
| 877 |
-
"Your response MUST include ALL of these:\n"
|
| 878 |
-
"1. **Problem Summary** β 2-3 sentences about their specific situation\n"
|
| 879 |
-
"2. **Root Cause Analysis** β what you believe is causing the problem\n"
|
| 880 |
-
"3. **Step-by-Step Action Plan** with NUMBERED steps organized by timeline:\n"
|
| 881 |
-
" - **Immediate (This Week):** 2-3 quick wins\n"
|
| 882 |
-
" - **Short-Term (Next 2-4 Weeks):** 3-4 tactical changes\n"
|
| 883 |
-
" - **Medium-Term (1-3 Months):** 2-3 strategic initiatives\n"
|
| 884 |
-
"4. **Expected Outcomes** β what results they can expect\n"
|
| 885 |
-
"Each step must name SPECIFIC tools, methods, platforms. NOT vague advice.\n"
|
| 886 |
-
"This response should be LONG and DETAILED β at least 300 words."
|
| 887 |
))
|
| 888 |
else:
|
| 889 |
expand_msg = SystemMessage(content=(
|
| 890 |
-
"β οΈ Your response is
|
| 891 |
-
"
|
| 892 |
-
"
|
| 893 |
-
"- A **bold header** like '**Key Observations**' followed by 3-4 bullet points\n"
|
| 894 |
-
" with specific insights, industry knowledge, or patterns you've noticed\n"
|
| 895 |
-
"- 1-2 sentences of concrete recommendation or context\n"
|
| 896 |
-
"- THEN your question(s)\n"
|
| 897 |
-
"Total response before questions should be at least 5-8 sentences."
|
| 898 |
))
|
| 899 |
messages.insert(-1, expand_msg)
|
| 900 |
response = await llm.ainvoke(messages)
|
|
@@ -912,10 +1036,16 @@ async def run_consultation(session_state: SessionState, user_message: str) -> Gr
|
|
| 912 |
# 3.9. Strip leading filler sentences (no LLM call)
|
| 913 |
parsed["reply"] = _strip_filler_sentences(parsed["reply"])
|
| 914 |
|
| 915 |
-
# 3.10.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 916 |
parsed["reply"] = _break_wall_of_text(parsed["reply"])
|
| 917 |
|
| 918 |
-
# 3.
|
| 919 |
parsed["reply"] = _format_questions(parsed["reply"])
|
| 920 |
|
| 921 |
# 4. Confidence progression
|
|
@@ -933,12 +1063,17 @@ async def run_consultation(session_state: SessionState, user_message: str) -> Gr
|
|
| 933 |
# 5. Route phase
|
| 934 |
new_phase = _determine_phase(new_confidence)
|
| 935 |
|
| 936 |
-
# Phase forcing β
|
| 937 |
topic_turns = turn_number - session_state.topic_start_turn
|
| 938 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 939 |
new_phase = Phase.REFINEMENT
|
| 940 |
new_confidence = max(new_confidence, 0.85)
|
| 941 |
-
elif topic_turns >= 10 and new_phase in (Phase.DISCOVERY, Phase.EXPLORATION):
|
| 942 |
new_phase = Phase.SOLUTION
|
| 943 |
new_confidence = max(new_confidence, 0.65)
|
| 944 |
# 6. Update understanding (with validation)
|
|
|
|
| 1 |
+
"""LangGraph-based requirement-collection consultation state machine.
|
| 2 |
|
| 3 |
+
Implements a 5-phase flow (Discovery β Exploration β Constraints β Solution β Refinement)
|
| 4 |
+
controlled by confidence. Supports both clear requests (e.g. build X) and problems (e.g. Y is wrong).
|
| 5 |
|
| 6 |
CODE-LEVEL QUALITY ENFORCEMENT:
|
| 7 |
+
- Requirement-oriented question categories (scope, what they have, behavior/look, success)
|
| 8 |
+
- Anti-repetition: sentence overlap detection and re-generation
|
| 9 |
+
- Anti-hallucination: verifies claims against conversation history
|
| 10 |
+
- Deferral counter: "I don't know" β switch to proposal mode
|
| 11 |
+
- Turn-aware instructions and understanding validation
|
|
|
|
| 12 |
"""
|
| 13 |
|
| 14 |
from __future__ import annotations
|
|
|
|
| 22 |
|
| 23 |
from llm import llm
|
| 24 |
from models import Message, Phase, SessionState, PHASE_ORDER, PHASE_THRESHOLDS
|
| 25 |
+
from prompts import PHASE_PROMPTS, QUESTIONNAIRE_SECTIONS, CURRENT_SECTION_PLACEHOLDER
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
# ---------------------------------------------------------------------------
|
| 29 |
+
# Questionnaire-driven section progression (same intent β same question order)
|
| 30 |
+
# ---------------------------------------------------------------------------
|
| 31 |
+
|
| 32 |
+
def _current_section_index(questions_asked: int) -> int:
|
| 33 |
+
"""Section index 0..10 from number of questions asked. ~2 questions per section."""
|
| 34 |
+
return min(questions_asked // 2, len(QUESTIONNAIRE_SECTIONS) - 1)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def _build_section_block(section_index: int) -> str:
|
| 38 |
+
"""Build the 'Current section' block to inject into the prompt."""
|
| 39 |
+
num, title, intents = QUESTIONNAIRE_SECTIONS[section_index]
|
| 40 |
+
intents_str = "\n".join(f"- {i}" for i in intents)
|
| 41 |
+
return (
|
| 42 |
+
f"Current section: Section {num} β {title}\n"
|
| 43 |
+
f"Question intents to cover (adapt to domain and conversation):\n"
|
| 44 |
+
f"{intents_str}\n"
|
| 45 |
+
f"If the client has ALREADY answered these intents earlier in the conversation, skip this section or ask only what's missing, then move to the next section.\n"
|
| 46 |
+
)
|
| 47 |
|
| 48 |
|
| 49 |
# ---------------------------------------------------------------------------
|
|
|
|
| 60 |
|
| 61 |
|
| 62 |
# ---------------------------------------------------------------------------
|
| 63 |
+
# Wrap-up guidance (when to ask "Is there anything else?")
|
| 64 |
# ---------------------------------------------------------------------------
|
| 65 |
|
| 66 |
+
def _build_wrap_up_guidance(questions_asked: int, messages: list[Message], section_index: int) -> str:
|
| 67 |
+
"""Tell the bot when to keep gathering vs when to ask 'Is there anything else?'"""
|
| 68 |
+
if _can_wrap_up(questions_asked, messages):
|
| 69 |
+
return "\nYou've gathered enough info. Ask: 'Is there anything else you'd like to add?' Only that β no proposal or solution.\n"
|
| 70 |
+
section_num = section_index + 1
|
| 71 |
+
total_sections = len(QUESTIONNAIRE_SECTIONS)
|
| 72 |
+
return (
|
| 73 |
+
f"\nKeep gathering. You are on section {section_num} of {total_sections} (follow the questionnaire in order). "
|
| 74 |
+
f"Do NOT ask 'Is there anything else?' until you have gone through ALL 11 sections (through Section 11 - Open Ends) "
|
| 75 |
+
f"and asked at least {MIN_QUESTIONS_BEFORE_WRAP_UP} questions. You have asked {questions_asked} so far.\n"
|
| 76 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
|
| 78 |
|
| 79 |
# ---------------------------------------------------------------------------
|
|
|
|
| 146 |
for msg in messages:
|
| 147 |
if msg.role == "assistant" and "?" in msg.content:
|
| 148 |
content_lower = msg.content.lower()
|
|
|
|
|
|
|
| 149 |
question_parts = content_lower.split("?")
|
| 150 |
for part in question_parts:
|
| 151 |
for pattern in DATA_STORAGE_QUESTION_PATTERNS:
|
|
|
|
| 154 |
return False
|
| 155 |
|
| 156 |
|
| 157 |
+
# Require ALL 11 sections before wrap-up
|
| 158 |
+
MIN_QUESTIONS_BEFORE_WRAP_UP = 22 # ~2 per section so we reach Section 11
|
| 159 |
+
MIN_SECTION_BEFORE_WRAP_UP = 10 # Section 11 (Open Ends) β must complete all sections
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
def _can_wrap_up(questions_asked: int, messages: list[Message]) -> bool:
|
| 163 |
+
"""True only after we've gone through ALL 11 questionnaire sections (no early exit)."""
|
| 164 |
+
section_index = _current_section_index(questions_asked)
|
| 165 |
+
# Must reach Section 11 (Open Ends, index 10) and ask at least 22 questions
|
| 166 |
+
return (
|
| 167 |
+
questions_asked >= MIN_QUESTIONS_BEFORE_WRAP_UP
|
| 168 |
+
and section_index >= MIN_SECTION_BEFORE_WRAP_UP
|
| 169 |
+
)
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
def _user_has_said_where_data_is(messages: list[Message]) -> bool:
|
| 173 |
+
"""True if the user has already said where they keep their data (spreadsheet, database, etc.)."""
|
| 174 |
+
data_location_words = [
|
| 175 |
+
"spreadsheet", "database", "excel", "notebook", "paper", "software",
|
| 176 |
+
"app", "system", "cloud", "computer", "file", "files",
|
| 177 |
+
]
|
| 178 |
+
for msg in messages:
|
| 179 |
+
if msg.role != "user":
|
| 180 |
+
continue
|
| 181 |
+
lower = msg.content.lower().strip()
|
| 182 |
+
# Short reply that looks like an answer (e.g. "spreadsheet", "database")
|
| 183 |
+
if len(lower) < 80 and any(w in lower for w in data_location_words):
|
| 184 |
+
return True
|
| 185 |
+
if any(w in lower for w in data_location_words) and ("keep" in lower or "store" in lower or "track" in lower or "use" in lower or "in " in lower):
|
| 186 |
+
return True
|
| 187 |
+
return False
|
| 188 |
+
|
| 189 |
+
|
| 190 |
def _check_repetition(new_reply: str, previous_replies: list[str]) -> float:
|
| 191 |
"""Check sentence-level overlap with previous replies. Returns 0.0-1.0."""
|
| 192 |
if not previous_replies:
|
|
|
|
| 228 |
# Reply quality enforcement (code-level, can't be bypassed by LLM)
|
| 229 |
# ---------------------------------------------------------------------------
|
| 230 |
|
| 231 |
+
# Budget / timeline β NEVER ask (policy)
|
| 232 |
+
BUDGET_TIMELINE_PATTERNS = [
|
| 233 |
+
r"budget",
|
| 234 |
+
r"timeline",
|
| 235 |
+
r"when (?:do you |would you |)need (?:it|this|the)",
|
| 236 |
+
r"deadline",
|
| 237 |
+
r"how (?:much |long |)(?:time|money)",
|
| 238 |
+
r"expected (?:timeline|date|duration)",
|
| 239 |
+
r"time (?:frame|line|scale)",
|
| 240 |
+
r"(?:within|in) (?:how many|what) (?:weeks|months|days)",
|
| 241 |
+
r"expectations? (?:for|about) (?:the )?timeline",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 242 |
]
|
| 243 |
|
| 244 |
+
# Policy-only checks: budget/timeline and tech. All other behavior is guided by prompts and LLM context.
|
| 245 |
+
# Non-tech client: no "involvement", "development", "implementation", "progress updates", "system design"
|
| 246 |
+
TECH_PROCESS_PATTERNS = [
|
| 247 |
+
r"involved in (?:the |)(?:development|implementation)",
|
| 248 |
+
r"involvement in (?:development|implementation|design)",
|
| 249 |
+
r"progress updates?",
|
| 250 |
+
r"regular (?:meetings?|updates?)",
|
| 251 |
+
r"system design",
|
| 252 |
+
r"input on (?:system |)(?:design|implementation)",
|
| 253 |
+
r"project management (?:portal|tool)",
|
| 254 |
+
r"preferred method for (?:receiving|getting) (?:updates?|progress)",
|
| 255 |
+
r"receiving (?:progress )?updates?",
|
| 256 |
+
r"providing input on",
|
| 257 |
+
r"implementation (?:of the |)(?:system|solution)",
|
| 258 |
+
r"development and implementation",
|
| 259 |
+
r"first meeting to discuss (?:project|details|expectations)",
|
| 260 |
+
r"ready to proceed",
|
| 261 |
+
r"prioritize .*(?:goals?|objectives?)",
|
| 262 |
]
|
| 263 |
|
| 264 |
# Filler SENTENCES that should be stripped from ANYWHERE in the response
|
|
|
|
| 270 |
# "I'd love to" filler
|
| 271 |
r"i'd love to (?:learn|help|explore|understand|hear|know|dig)",
|
| 272 |
r"i'd like to (?:learn|help|explore|understand|hear|know|dig)",
|
| 273 |
+
# Restating the problem robotically (domain-agnostic)
|
|
|
|
| 274 |
r"(?:can be|is) a (?:challenging|significant|difficult|tough|particularly challenging|common) (?:issue|problem|situation|challenge)",
|
| 275 |
r"(?:this|it) can be a (?:common|challenging|significant) (?:issue|problem|situation)",
|
| 276 |
+
r"especially (?:given|with) the (?:high value|constantly changing|competitive)",
|
| 277 |
+
r"especially given the (?:high value|unique|complex) (?:nature|aspects)",
|
| 278 |
+
r"(?:market|industry) can be (?:often |)(?:subject to|a (?:high|competitive))",
|
| 279 |
+
r"are often (?:purchased|used|needed) for (?:special |)(?:occasions|reasons)",
|
| 280 |
+
r"the (?:quality and )?characteristics of the",
|
| 281 |
+
r"the (?:market|industry) can be",
|
| 282 |
r"it's likely that this is affecting",
|
| 283 |
r"this (?:can|may|might) (?:be|also be) (?:affecting|impacting|taking)",
|
| 284 |
# Robotic transitions
|
|
|
|
| 298 |
r"given the (?:specifics|broad range|unique aspects|diverse nature|complexity)",
|
| 299 |
r"to better understand (?:the scope|your situation|the issue|your)",
|
| 300 |
r"this (?:is a good starting point|indicates you have)",
|
| 301 |
+
r"it'?s possible that .+(?:could be|might be|is)",
|
| 302 |
+
r"(?:could be|might be) (?:impacting|affecting|contributing)",
|
| 303 |
+
r"(?:understanding|this information) (?:the specifics|helps us understand)",
|
| 304 |
+
r"can (?:help|provide) (?:identify|valuable insights)",
|
| 305 |
+
r"understanding the (?:current state|specifics) .+ (?:can provide|can help)",
|
| 306 |
r"i'm excited to learn more",
|
| 307 |
r"i'm looking forward to",
|
| 308 |
r"understanding (?:your|the) specific",
|
|
|
|
| 331 |
r"it(?:'s| is) great that you(?:'ve| have)",
|
| 332 |
# "You've just started our conversation"
|
| 333 |
r"you(?:'ve| have) just started",
|
| 334 |
+
# Solution pitching β requirement gathering only
|
| 335 |
+
r"our approach (?:at |)stacklogix (?:involves|would involve|will involve)",
|
| 336 |
+
r"stacklogix will (?:develop|implement|provide|deliver)",
|
| 337 |
+
r"how we will address (?:it|this)",
|
| 338 |
+
r"what we will deliver",
|
| 339 |
+
r"implementation (?:of a |)(?:robust |)(?:management |)system",
|
| 340 |
+
r"key components? of the solution",
|
| 341 |
+
r"we will (?:develop|implement|provide|schedule|work closely)",
|
| 342 |
]
|
| 343 |
|
| 344 |
# Section headers that create robotic formatting
|
|
|
|
| 350 |
|
| 351 |
|
| 352 |
def _detect_bad_question_patterns(reply: str) -> str | None:
|
| 353 |
+
"""Detect self-solving, cause-suggesting, budget/timeline, or technical process questions.
|
| 354 |
|
| 355 |
Returns a correction instruction if bad patterns found, None otherwise.
|
| 356 |
"""
|
| 357 |
reply_lower = reply.lower()
|
| 358 |
|
| 359 |
+
# Only enforce hard policy via patterns; let the LLM handle the rest from conversation context.
|
| 360 |
+
for pattern in BUDGET_TIMELINE_PATTERNS:
|
| 361 |
if re.search(pattern, reply_lower):
|
| 362 |
return (
|
| 363 |
+
"β οΈ You must NOT ask about budget or timeline. "
|
| 364 |
+
"Remove any question about when they need it, deadlines, or cost. Ask something else from the questionnaire in plain words."
|
|
|
|
|
|
|
|
|
|
| 365 |
)
|
| 366 |
+
|
| 367 |
+
for pattern in TECH_PROCESS_PATTERNS:
|
| 368 |
if re.search(pattern, reply_lower):
|
| 369 |
return (
|
| 370 |
+
"β οΈ The client is non-technical. Do NOT ask about involvement in development, "
|
| 371 |
+
"progress updates, system design, project management, or implementation. "
|
| 372 |
+
"Ask a simple, plain-language question from the current section."
|
|
|
|
| 373 |
)
|
| 374 |
+
|
| 375 |
return None
|
| 376 |
|
| 377 |
|
|
|
|
| 469 |
return "\n\n".join(result)
|
| 470 |
|
| 471 |
|
| 472 |
+
def _strip_observation_blocks(reply: str) -> str:
|
| 473 |
+
"""Remove 'Key Observations', 'Understanding the situation', and similar blocks with bullet lists.
|
| 474 |
+
|
| 475 |
+
Keep only short acknowledgment and questions. Skip until we see a question line.
|
| 476 |
+
"""
|
| 477 |
+
lines = reply.split("\n")
|
| 478 |
+
result = []
|
| 479 |
+
in_observation_block = False
|
| 480 |
+
|
| 481 |
+
for line in lines:
|
| 482 |
+
stripped = line.strip()
|
| 483 |
+
# Start of an observation/recap block (bold header, no question)
|
| 484 |
+
if re.match(r"^\*\*(?:key observations?|understanding the situation|key points?|current (?:understanding|situation)|summary)\s*", stripped, re.IGNORECASE) and "?" not in stripped:
|
| 485 |
+
in_observation_block = True
|
| 486 |
+
continue
|
| 487 |
+
# While in block: skip bullets and any non-question line (transition sentences, etc.)
|
| 488 |
+
if in_observation_block:
|
| 489 |
+
if stripped.startswith("-") or stripped.startswith("*") or re.match(r"^[\d]+[.)]\s", stripped):
|
| 490 |
+
continue
|
| 491 |
+
if "?" not in stripped:
|
| 492 |
+
continue
|
| 493 |
+
in_observation_block = False
|
| 494 |
+
result.append(line)
|
| 495 |
+
|
| 496 |
+
text = "\n".join(result)
|
| 497 |
+
text = re.sub(r"\n{3,}", "\n\n", text)
|
| 498 |
+
return text.strip()
|
| 499 |
+
|
| 500 |
+
|
| 501 |
+
def _extract_question_sentences(text: str) -> list[str]:
|
| 502 |
+
"""Get lines or sentence fragments that contain a question mark (normalized for comparison)."""
|
| 503 |
+
questions = []
|
| 504 |
+
for line in text.split("\n"):
|
| 505 |
+
part = line.strip().lower()
|
| 506 |
+
if "?" in part and len(part) > 15:
|
| 507 |
+
questions.append(part)
|
| 508 |
+
return questions
|
| 509 |
+
|
| 510 |
+
|
| 511 |
+
def _check_question_repetition(new_reply: str, previous_replies: list[str]) -> bool:
|
| 512 |
+
"""True if the new reply asks something very similar to a previous reply's question."""
|
| 513 |
+
new_qs = _extract_question_sentences(new_reply)
|
| 514 |
+
if not new_qs:
|
| 515 |
+
return False
|
| 516 |
+
all_prev_qs = []
|
| 517 |
+
for prev in previous_replies:
|
| 518 |
+
all_prev_qs.extend(_extract_question_sentences(prev))
|
| 519 |
+
if not all_prev_qs:
|
| 520 |
+
return False
|
| 521 |
+
for nq in new_qs:
|
| 522 |
+
for pq in all_prev_qs:
|
| 523 |
+
if SequenceMatcher(None, nq, pq).ratio() > 0.55:
|
| 524 |
+
return True
|
| 525 |
+
return False
|
| 526 |
+
|
| 527 |
+
|
| 528 |
+
def _ensure_text_before_questions(reply: str) -> str:
|
| 529 |
+
"""If the reply starts with a question (no preceding acknowledgment), prepend a short line."""
|
| 530 |
+
stripped = reply.strip()
|
| 531 |
+
if not stripped:
|
| 532 |
+
return reply
|
| 533 |
+
# Find first line that contains ?
|
| 534 |
+
lines = stripped.split("\n")
|
| 535 |
+
first_question_idx = None
|
| 536 |
+
for i, line in enumerate(lines):
|
| 537 |
+
if "?" in line and len(line.strip()) > 5:
|
| 538 |
+
first_question_idx = i
|
| 539 |
+
break
|
| 540 |
+
if first_question_idx is None:
|
| 541 |
+
return reply
|
| 542 |
+
# Is there any non-empty, non-question text before the first question?
|
| 543 |
+
text_before = "\n".join(lines[:first_question_idx]).strip()
|
| 544 |
+
text_before = re.sub(r"\*\*[^*]+\*\*", "", text_before).strip() # remove bold markers for length check
|
| 545 |
+
if len(text_before) < 50: # require at least ~2 sentences of context before first question
|
| 546 |
+
lines.insert(0, "Thanks for sharing that.")
|
| 547 |
+
return "\n".join(lines).strip()
|
| 548 |
+
return reply
|
| 549 |
+
|
| 550 |
+
|
| 551 |
def _format_questions(reply: str) -> str:
|
| 552 |
"""Ensure each question is on its own paragraph with blank lines around it.
|
| 553 |
|
|
|
|
| 697 |
# Keep the conversation history but mark the new topic
|
| 698 |
state.understanding = state.understanding + f"\n\n--- NEW TOPIC: {user_message} ---\n" if state.understanding else ""
|
| 699 |
|
| 700 |
+
# --- HARD PHASE FORCING β require minimum 10 questions (or lots of client info) before wrap-up ---
|
|
|
|
| 701 |
topic_turns = turn_number - state.topic_start_turn
|
| 702 |
current_phase = state.phase
|
| 703 |
current_confidence = state.confidence
|
| 704 |
+
if not _can_wrap_up(questions_asked, state.messages) and current_phase in (Phase.SOLUTION, Phase.REFINEMENT):
|
| 705 |
+
# Keep in EXPLORATION until we've asked at least 10 questions (or client gave lots of info)
|
| 706 |
+
current_phase = Phase.EXPLORATION
|
| 707 |
+
current_confidence = min(current_confidence, 0.5)
|
| 708 |
+
state.phase = current_phase
|
| 709 |
+
state.confidence = current_confidence
|
| 710 |
+
elif topic_turns >= 12 and current_phase != Phase.REFINEMENT:
|
| 711 |
current_phase = Phase.REFINEMENT
|
| 712 |
current_confidence = max(current_confidence, 0.85)
|
| 713 |
state.phase = current_phase
|
|
|
|
| 724 |
phase_prompt = TURN_1_PROMPT
|
| 725 |
else:
|
| 726 |
phase_prompt = PHASE_PROMPTS[current_phase].format(confidence=current_confidence)
|
| 727 |
+
# Inject current questionnaire section so the same intent leads to the same question order
|
| 728 |
+
section_index = _current_section_index(questions_asked)
|
| 729 |
+
if current_phase in (Phase.DISCOVERY, Phase.EXPLORATION, Phase.CONSTRAINTS):
|
| 730 |
+
phase_prompt = phase_prompt.replace(CURRENT_SECTION_PLACEHOLDER, _build_section_block(section_index))
|
| 731 |
+
else:
|
| 732 |
+
phase_prompt = phase_prompt.replace(CURRENT_SECTION_PLACEHOLDER, "(Wrapping up β ask if there is anything else.)")
|
| 733 |
|
| 734 |
# Add understanding context (validated)
|
| 735 |
if state.understanding:
|
| 736 |
phase_prompt += (
|
| 737 |
+
f"\n\nCLIENT INFO (for YOUR reference only β do NOT copy this back to the client):"
|
| 738 |
f"\n{state.understanding}\n"
|
| 739 |
+
f"Do NOT list 'Key Points', 'Summary', or 'How we will address it'. Just ask your next question(s) in simple words.\n"
|
|
|
|
| 740 |
)
|
| 741 |
+
# Remind LLM to use the full conversation (messages below) to adapt and improve
|
| 742 |
+
if turn_number > 1:
|
| 743 |
+
phase_prompt += "\nThe messages below are the full conversation. Use them to understand what the client said and what you already asked; avoid repeating, infer context, and choose the next 1β2 questions that add the most value.\n"
|
| 744 |
|
| 745 |
# --- CONVERSATION STATS ---
|
| 746 |
stats = f"\nTurn {turn_number} | {questions_asked} questions asked so far | {deferral_count} deferrals\n"
|
| 747 |
+
sec_idx = _current_section_index(questions_asked)
|
| 748 |
+
if not _can_wrap_up(questions_asked, state.messages):
|
| 749 |
+
stats += f"Follow the questionnaire: you are on section {sec_idx + 1} of {len(QUESTIONNAIRE_SECTIONS)}. Do NOT ask 'Is there anything else?' until you have completed ALL 11 sections and asked at least {MIN_QUESTIONS_BEFORE_WRAP_UP} questions.\n"
|
| 750 |
phase_prompt += stats
|
| 751 |
|
| 752 |
+
# --- ANTI-REPEAT REMINDER --- Follow the current section only; do not repeat past topics
|
| 753 |
+
if turn_number >= 4:
|
| 754 |
+
phase_prompt += "\nβ οΈ Do NOT repeat questions from earlier sections. Ask only from the current section above. If the client already answered or said 'I don't know', move to the next intent in the section or the next section.\n"
|
| 755 |
+
# --- "I ALREADY ANSWERED" --- Do not re-ask; move to next intent or section
|
| 756 |
+
if re.search(r"already (?:answered|said|mentioned|told)|i (?:already )?said (?:that )?(?:already )?|said (?:that )?already", user_msg_lower):
|
| 757 |
+
phase_prompt += "\nβ οΈ The client said they already answered. Acknowledge briefly (e.g. 'Got it.') and move to the NEXT intent or next section. Do NOT ask the same or a similar question again.\n"
|
| 758 |
+
|
| 759 |
+
# --- DATA: do not repeat once they've said where data is (questionnaire Section 4 covers this) ---
|
| 760 |
+
if _user_has_said_where_data_is(state.messages):
|
| 761 |
phase_prompt += """
|
| 762 |
+
β οΈ The client has ALREADY said where they keep their data (e.g. spreadsheet, database, notebook). Do NOT ask again about where it's stored, what it looks like, or where the file is. Continue with the current section or move on.\n"""
|
| 763 |
+
# --- WRAP-UP GUIDANCE (questionnaire sections drive what to ask; this only says when to end) ---
|
| 764 |
+
if turn_number > 1:
|
| 765 |
+
sec_idx = _current_section_index(questions_asked)
|
| 766 |
+
wrap_guidance = _build_wrap_up_guidance(questions_asked, state.messages, sec_idx)
|
| 767 |
+
phase_prompt += wrap_guidance
|
|
|
|
|
|
|
|
|
|
| 768 |
|
| 769 |
# --- DYNAMIC BEHAVIORAL OVERRIDES ---
|
| 770 |
if deferral_count >= 2:
|
| 771 |
phase_prompt += """
|
| 772 |
+
β οΈ THE USER SAID "I DON'T KNOW" MULTIPLE TIMES. Move on β do not keep asking that.
|
| 773 |
+
- Skip that topic. Ask about a DIFFERENT requirement (e.g. where they keep their data, or what they want in simple terms).
|
| 774 |
+
- Do NOT pitch a solution or explain "how we will address it". Just gather what you can and then ask "Is there anything else?"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 775 |
"""
|
| 776 |
|
| 777 |
# Question count limits only apply during diagnostic phases
|
| 778 |
# In solution/final phases, the prompt itself handles question limits
|
| 779 |
if state.phase in (Phase.DISCOVERY, Phase.EXPLORATION):
|
| 780 |
if questions_asked >= 20:
|
| 781 |
+
phase_prompt += "\nβ οΈ STOP ASKING. Ask only: 'Is there anything else you'd like to add?' Nothing else. No proposal.\n"
|
| 782 |
elif questions_asked >= 14:
|
| 783 |
+
phase_prompt += "\nYou've asked enough. Ask: 'Is there anything else you'd like to add?' Only that β no solution or approach.\n"
|
| 784 |
|
| 785 |
if turn_number >= 8:
|
| 786 |
+
phase_prompt += "\nβ οΈ Turn 8+. Ask 'Is there anything else you'd like to add?' Do NOT give a proposal or solution.\n"
|
| 787 |
|
| 788 |
messages = [SystemMessage(content=phase_prompt)]
|
| 789 |
|
|
|
|
| 888 |
return Phase.DISCOVERY
|
| 889 |
|
| 890 |
|
| 891 |
+
# Thank-you message when client says "no" to "Is there anything else?"
|
| 892 |
+
THANK_YOU_MESSAGE = (
|
| 893 |
+
"Thank you. We have everything we need. Our team at Stacklogix will be in touch with you shortly."
|
| 894 |
+
)
|
| 895 |
+
|
| 896 |
+
# User said "no" / "nothing else" / "bye" to "Is there anything else?" β respond with thank-you ONCE, do not ask again
|
| 897 |
+
ANYTHING_ELSE_NO_PATTERNS = [
|
| 898 |
+
r"^\s*no\s*$", r"^\s*nope\s*$", r"^\s*nothing\s*$",
|
| 899 |
+
r"^\s*that'?s? (?:all|it)\s*$", r"^\s*that is all\s*$",
|
| 900 |
+
r"^\s*no\s*,?\s*thanks?\s*$", r"^\s*thanks?\s*,?\s*that'?s? all\s*$",
|
| 901 |
+
r"^\s*(?:we'?re|i'?m) good\s*$", r"^\s*all good\s*$",
|
| 902 |
+
r"^\s*not really\s*$", r"^\s*nothing else\s*$",
|
| 903 |
+
r"^\s*no\s+not\s+at\s+all\s*$", r"^\s*not\s+at\s+all\s*$",
|
| 904 |
+
r"^\s*bye\s*$", r"^\s*i'?m\s+good\s*$", r"^\s*that'?s\s+it\s*$",
|
| 905 |
+
]
|
| 906 |
+
|
| 907 |
+
|
| 908 |
+
def _last_bot_asked_anything_else(messages: list[Message]) -> bool:
|
| 909 |
+
"""True if the last assistant message asks 'anything else'."""
|
| 910 |
+
for msg in reversed(messages):
|
| 911 |
+
if msg.role == "assistant":
|
| 912 |
+
lower = msg.content.lower()
|
| 913 |
+
return "anything else" in lower or "anything more" in lower
|
| 914 |
+
return False
|
| 915 |
+
|
| 916 |
+
|
| 917 |
+
def _user_said_no_to_anything_else(user_message: str) -> bool:
|
| 918 |
+
"""True if user is saying no / nothing else."""
|
| 919 |
+
stripped = user_message.strip().lower()
|
| 920 |
+
return any(re.search(p, stripped, re.IGNORECASE) for p in ANYTHING_ELSE_NO_PATTERNS)
|
| 921 |
+
|
| 922 |
+
|
| 923 |
# ---------------------------------------------------------------------------
|
| 924 |
# Main graph execution
|
| 925 |
# ---------------------------------------------------------------------------
|
|
|
|
| 927 |
async def run_consultation(session_state: SessionState, user_message: str) -> GraphState:
|
| 928 |
"""Run one turn of the consultation with quality enforcement."""
|
| 929 |
|
| 930 |
+
# 0. If bot just asked "Is there anything else?" and user said no β thank you and end
|
| 931 |
+
if session_state.messages and _last_bot_asked_anything_else(session_state.messages) and _user_said_no_to_anything_else(user_message):
|
| 932 |
+
session_state.messages.append(Message(role="user", content=user_message))
|
| 933 |
+
session_state.messages.append(Message(role="assistant", content=THANK_YOU_MESSAGE))
|
| 934 |
+
return GraphState(
|
| 935 |
+
session_state=session_state,
|
| 936 |
+
user_message=user_message,
|
| 937 |
+
assistant_reply=THANK_YOU_MESSAGE,
|
| 938 |
+
new_confidence=1.0,
|
| 939 |
+
new_understanding=session_state.understanding,
|
| 940 |
+
new_phase=Phase.REFINEMENT,
|
| 941 |
+
)
|
| 942 |
+
|
| 943 |
# 1. Build messages with intelligence injection
|
| 944 |
messages = _build_message_list(session_state, user_message)
|
| 945 |
turn_number = _get_turn_number(session_state.messages)
|
|
|
|
| 948 |
response = await llm.ainvoke(messages)
|
| 949 |
parsed = _parse_llm_output(response.content)
|
| 950 |
|
| 951 |
+
# 3. Anti-repetition check (content and questions)
|
| 952 |
previous_replies = _get_previous_bot_responses(session_state.messages)
|
| 953 |
overlap = _check_repetition(parsed["reply"], previous_replies)
|
| 954 |
+
question_repeat = _check_question_repetition(parsed["reply"], previous_replies)
|
| 955 |
|
| 956 |
+
if (overlap > 0.35 or question_repeat) and previous_replies:
|
| 957 |
anti_repeat_msg = SystemMessage(content=(
|
| 958 |
+
"β οΈ You are REPEATING yourself. Either your content or your questions are too similar to what you already asked.\n"
|
| 959 |
+
"The client has already answered (or said 'I don't know') to topics you've covered. Do NOT ask about those again.\n"
|
| 960 |
+
"Reply with ONLY 1-2 short sentences, then ask about something you have NOT asked yet (from the current questionnaire section), OR ask: 'Is there anything else you'd like to add?'\n"
|
| 961 |
+
"No 'Key Observations' or bullet lists. Just a short acknowledgment and one new question."
|
|
|
|
| 962 |
))
|
| 963 |
messages.insert(-1, anti_repeat_msg)
|
| 964 |
response = await llm.ainvoke(messages)
|
|
|
|
| 1004 |
non_q_lines = [line for line in reply_text.split("\n") if "?" not in line]
|
| 1005 |
non_q_content = "\n".join(non_q_lines).strip()
|
| 1006 |
|
| 1007 |
+
# SOLUTION/REFINEMENT = only "Is there anything else?" β keep it short
|
| 1008 |
is_final_phase = session_state.phase in (Phase.SOLUTION, Phase.REFINEMENT)
|
| 1009 |
+
min_length = 80 if is_final_phase else 120 # require at least 2-3 sentences before questions
|
| 1010 |
|
| 1011 |
if len(non_q_content) < min_length:
|
| 1012 |
if is_final_phase:
|
| 1013 |
expand_msg = SystemMessage(content=(
|
| 1014 |
+
"β οΈ You must ONLY ask: 'Is there anything else you'd like to add?'\n"
|
| 1015 |
+
"Do NOT give a summary, proposal, or 'how we will address it'. Requirement gathering only. Short reply with that one question."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1016 |
))
|
| 1017 |
else:
|
| 1018 |
expand_msg = SystemMessage(content=(
|
| 1019 |
+
"β οΈ Your response is too short. You MUST include 2-3 sentences of acknowledgment before your question(s). "
|
| 1020 |
+
"Briefly reflect what they said and why it matters for understanding their needs. Then ask your question(s). "
|
| 1021 |
+
"Do NOT add 'Key Observations' or bullet lists. No single-line intros."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1022 |
))
|
| 1023 |
messages.insert(-1, expand_msg)
|
| 1024 |
response = await llm.ainvoke(messages)
|
|
|
|
| 1036 |
# 3.9. Strip leading filler sentences (no LLM call)
|
| 1037 |
parsed["reply"] = _strip_filler_sentences(parsed["reply"])
|
| 1038 |
|
| 1039 |
+
# 3.10. Strip "Key Observations" and similar observation/recap blocks
|
| 1040 |
+
parsed["reply"] = _strip_observation_blocks(parsed["reply"])
|
| 1041 |
+
|
| 1042 |
+
# 3.10b. Ensure there is some text before the first question (no bare questions)
|
| 1043 |
+
parsed["reply"] = _ensure_text_before_questions(parsed["reply"])
|
| 1044 |
+
|
| 1045 |
+
# 3.11. Break wall-of-text responses into readable paragraphs
|
| 1046 |
parsed["reply"] = _break_wall_of_text(parsed["reply"])
|
| 1047 |
|
| 1048 |
+
# 3.12. Ensure questions are properly formatted for frontend box styling
|
| 1049 |
parsed["reply"] = _format_questions(parsed["reply"])
|
| 1050 |
|
| 1051 |
# 4. Confidence progression
|
|
|
|
| 1063 |
# 5. Route phase
|
| 1064 |
new_phase = _determine_phase(new_confidence)
|
| 1065 |
|
| 1066 |
+
# Phase forcing β require at least 10 questions (or lots of client info) before wrap-up
|
| 1067 |
topic_turns = turn_number - session_state.topic_start_turn
|
| 1068 |
+
questions_after_this_turn = _count_bot_questions(session_state.messages) + parsed["reply"].count("?")
|
| 1069 |
+
messages_incl_current = session_state.messages + [Message(role="user", content=user_message)]
|
| 1070 |
+
if not _can_wrap_up(questions_after_this_turn, messages_incl_current) and new_phase in (Phase.SOLUTION, Phase.REFINEMENT):
|
| 1071 |
+
new_phase = Phase.EXPLORATION
|
| 1072 |
+
new_confidence = min(new_confidence, 0.5)
|
| 1073 |
+
elif topic_turns >= 12 and _can_wrap_up(questions_after_this_turn, messages_incl_current):
|
| 1074 |
new_phase = Phase.REFINEMENT
|
| 1075 |
new_confidence = max(new_confidence, 0.85)
|
| 1076 |
+
elif topic_turns >= 10 and _can_wrap_up(questions_after_this_turn, messages_incl_current) and new_phase in (Phase.DISCOVERY, Phase.EXPLORATION):
|
| 1077 |
new_phase = Phase.SOLUTION
|
| 1078 |
new_confidence = max(new_confidence, 0.65)
|
| 1079 |
# 6. Update understanding (with validation)
|
backend/prompts.py
CHANGED
|
@@ -1,106 +1,100 @@
|
|
| 1 |
-
"""System prompts for
|
| 2 |
|
| 3 |
DESIGN PRINCIPLES:
|
| 4 |
-
-
|
| 5 |
-
-
|
| 6 |
-
- Never
|
| 7 |
-
-
|
|
|
|
| 8 |
"""
|
| 9 |
|
| 10 |
from __future__ import annotations
|
| 11 |
|
| 12 |
# ---------------------------------------------------------------------------
|
| 13 |
-
#
|
| 14 |
# ---------------------------------------------------------------------------
|
| 15 |
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
2
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
11. NEVER repeat content from previous turns.
|
| 98 |
-
|
| 99 |
-
12. Each question MUST cover a DIFFERENT aspect of the problem. Never ask two
|
| 100 |
-
questions about the same topic. Vary across: operations, finances, customers,
|
| 101 |
-
team, strategy, goals, market, competitors.
|
| 102 |
-
|
| 103 |
-
13. Be SPECIFIC in advice: name actual methods, steps, approaches. No vague advice.
|
| 104 |
"""
|
| 105 |
|
| 106 |
_OUTPUT_FORMAT = """
|
|
@@ -110,38 +104,34 @@ Respond with ONLY a JSON object:
|
|
| 110 |
"confidence": <float 0.0-1.0>,
|
| 111 |
"understanding": "Cumulative summary of ONLY what the client has explicitly stated. Do NOT add interpretations."
|
| 112 |
}}
|
| 113 |
-
Confidence:
|
| 114 |
"""
|
| 115 |
|
| 116 |
# ---------------------------------------------------------------------------
|
| 117 |
# TURN 1 β first response
|
| 118 |
# ---------------------------------------------------------------------------
|
| 119 |
|
|
|
|
|
|
|
|
|
|
| 120 |
TURN_1_PROMPT = (
|
| 121 |
_PERSONA
|
| 122 |
+ """
|
| 123 |
THIS IS THE USER'S VERY FIRST MESSAGE.
|
| 124 |
|
| 125 |
-
|
| 126 |
-
describing any problem, give a SHORT warm greeting (1-2 sentences max) and ask
|
| 127 |
-
what they'd like help with. Do NOT generate generic observations or filler.
|
| 128 |
|
| 129 |
-
If they
|
| 130 |
-
1. Start with a short, warm reaction to what they said (1 sentence)
|
| 131 |
-
2. Ask 2 natural follow-up questions about their situation β one about their
|
| 132 |
-
business/domain and one about the SPECIFIC details of the problem.
|
| 133 |
-
For example: "Which products are affected?" or "How long has this been happening?"
|
| 134 |
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
Do NOT
|
| 138 |
-
Do NOT generate generic observations like "every business challenge is unique".
|
| 139 |
"""
|
| 140 |
+ _OUTPUT_FORMAT
|
| 141 |
)
|
| 142 |
|
| 143 |
# ---------------------------------------------------------------------------
|
| 144 |
-
# Phase prompts β
|
| 145 |
# ---------------------------------------------------------------------------
|
| 146 |
|
| 147 |
DISCOVERY_PROMPT = (
|
|
@@ -149,19 +139,11 @@ DISCOVERY_PROMPT = (
|
|
| 149 |
+ """
|
| 150 |
PHASE: Discovery | Confidence: {confidence:.0%}
|
| 151 |
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
DIG INTO THE PROBLEM:
|
| 156 |
-
- If they mention a problem (e.g., slow inventory, low sales, high costs),
|
| 157 |
-
ask WHICH specific products/items/areas are affected.
|
| 158 |
-
- Ask about quantities, amounts, timelines β the concrete details.
|
| 159 |
-
- Ask what changed or when it started getting worse.
|
| 160 |
-
|
| 161 |
-
DO NOT ask about operations, data storage, team size, or processes yet.
|
| 162 |
-
Those come AFTER you understand exactly what's going wrong.
|
| 163 |
|
| 164 |
-
|
| 165 |
"""
|
| 166 |
+ _OUTPUT_FORMAT
|
| 167 |
)
|
|
@@ -171,19 +153,11 @@ EXPLORATION_PROMPT = (
|
|
| 171 |
+ """
|
| 172 |
PHASE: Exploration | Confidence: {confidence:.0%}
|
| 173 |
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
1. ROOT CAUSE β What specifically is causing the problem? Narrow it down.
|
| 178 |
-
Ask about: what changed, what they've already tried, which specific
|
| 179 |
-
parts of the business are most affected and why.
|
| 180 |
-
2. PAST ATTEMPTS β What have they tried to fix it? What happened?
|
| 181 |
-
3. DATA (only if not asked yet) β How do they track their business info?
|
| 182 |
-
|
| 183 |
-
Do NOT ask generic questions about team size, goals, or sales channels.
|
| 184 |
-
Stay focused on understanding WHY the problem exists.
|
| 185 |
|
| 186 |
-
|
| 187 |
"""
|
| 188 |
+ _OUTPUT_FORMAT
|
| 189 |
)
|
|
@@ -193,90 +167,47 @@ CONSTRAINTS_PROMPT = (
|
|
| 193 |
+ """
|
| 194 |
PHASE: Constraints | Confidence: {confidence:.0%}
|
| 195 |
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
- Reference ONLY details the client actually shared
|
| 200 |
-
- At most 1 question
|
| 201 |
|
| 202 |
-
|
| 203 |
"""
|
| 204 |
+ _OUTPUT_FORMAT
|
| 205 |
)
|
| 206 |
|
|
|
|
| 207 |
SOLUTION_PROMPT = (
|
| 208 |
_PERSONA
|
| 209 |
+ """
|
| 210 |
-
PHASE:
|
| 211 |
-
|
| 212 |
-
You now have enough information to deliver a DETAILED, ACTIONABLE solution.
|
| 213 |
|
| 214 |
-
|
| 215 |
|
| 216 |
-
|
| 217 |
-
their specific situation (reference details THEY told you).
|
| 218 |
|
| 219 |
-
|
| 220 |
-
what they've shared. Be specific, not generic.
|
| 221 |
|
| 222 |
-
|
| 223 |
-
take, organized by timeline:
|
| 224 |
-
- **Immediate (This Week):** 2-3 quick wins they can start today
|
| 225 |
-
- **Short-Term (Next 2-4 Weeks):** 3-4 tactical changes
|
| 226 |
-
- **Medium-Term (1-3 Months):** 2-3 strategic initiatives
|
| 227 |
-
|
| 228 |
-
Each step must be SPECIFIC: name actual tools, methods, platforms, strategies.
|
| 229 |
-
NOT "improve your marketing" but "Create an Instagram business account and
|
| 230 |
-
post 3-4 high-quality product photos per week with relevant hashtags."
|
| 231 |
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
Keep this response LONG and DETAILED β this is the value they came for.
|
| 235 |
-
At most 1 question to clarify implementation details.
|
| 236 |
"""
|
| 237 |
+ _OUTPUT_FORMAT
|
| 238 |
)
|
| 239 |
|
|
|
|
| 240 |
REFINEMENT_PROMPT = (
|
| 241 |
_PERSONA
|
| 242 |
+ """
|
| 243 |
PHASE: Final | Confidence: {confidence:.0%}
|
| 244 |
|
| 245 |
-
|
| 246 |
-
in the entire conversation β make it worth their time.
|
| 247 |
-
|
| 248 |
-
YOUR RESPONSE MUST INCLUDE:
|
| 249 |
-
|
| 250 |
-
1. **Executive Summary** β 3-4 sentences summarizing the problem and your
|
| 251 |
-
recommended approach. Reference specific details they shared.
|
| 252 |
-
|
| 253 |
-
2. **Detailed Action Pipeline** β A comprehensive NUMBERED step-by-step plan:
|
| 254 |
-
|
| 255 |
-
**Phase 1: Immediate Actions (This Week)**
|
| 256 |
-
- Step 1: [Specific action with details]
|
| 257 |
-
- Step 2: [Specific action with details]
|
| 258 |
-
- Step 3: [Specific action with details]
|
| 259 |
-
|
| 260 |
-
**Phase 2: Short-Term Strategy (Weeks 2-4)**
|
| 261 |
-
- Step 4: [Specific action with details]
|
| 262 |
-
- Step 5: [Specific action with details]
|
| 263 |
-
|
| 264 |
-
**Phase 3: Medium-Term Growth (Month 2-3)**
|
| 265 |
-
- Step 6: [Specific action with details]
|
| 266 |
-
- Step 7: [Specific action with details]
|
| 267 |
-
|
| 268 |
-
3. **Tools & Resources** β Specific tools, platforms, or resources they should
|
| 269 |
-
use (name actual products, websites, methods).
|
| 270 |
-
|
| 271 |
-
4. **Expected Results** β What outcomes to expect and rough timelines.
|
| 272 |
|
| 273 |
-
|
| 274 |
-
know if the plan is working.
|
| 275 |
|
| 276 |
-
|
| 277 |
|
| 278 |
-
|
| 279 |
-
"Is there anything else you'd like help with, or any other challenges you're facing?"
|
| 280 |
"""
|
| 281 |
+ _OUTPUT_FORMAT
|
| 282 |
)
|
|
|
|
| 1 |
+
"""System prompts for requirement gathering only (no solution pitching).
|
| 2 |
|
| 3 |
DESIGN PRINCIPLES:
|
| 4 |
+
- Pure requirement gathering: ask questions only. Do NOT explain approach, solution, or what Stacklogix will do.
|
| 5 |
+
- Always cover DATA when relevant: where it is or would be stored, what form it is in (e.g. list, spreadsheet, paper, software) β adapt to the client's situation (any domain).
|
| 6 |
+
- Never ask about budget or timeline.
|
| 7 |
+
- Client is typically non-technical: ask simple, plain-language questions only. No "system design", "implementation", "progress updates", "involved in development".
|
| 8 |
+
- When you have gathered enough, ask "Is there anything else you'd like to add?" If they say no, a thank-you will be sent.
|
| 9 |
"""
|
| 10 |
|
| 11 |
from __future__ import annotations
|
| 12 |
|
| 13 |
# ---------------------------------------------------------------------------
|
| 14 |
+
# Universal questionnaire β fixed order (no budget/timeline). Same intent β same flow.
|
| 15 |
# ---------------------------------------------------------------------------
|
| 16 |
|
| 17 |
+
# Principle-based sections: adapt to any domain from conversation context. No hardcoded examples.
|
| 18 |
+
QUESTIONNAIRE_SECTIONS = [
|
| 19 |
+
(1, "Intent & Context", [
|
| 20 |
+
"Infer from the client's words whether this is new or existing; do not ask them.",
|
| 21 |
+
"What they are trying to achieve, or for a problem ask about the subject in concrete terms that match what they said β stick to their topic, do not introduce unrelated angles.",
|
| 22 |
+
"Who it is for or one-sentence context, only if not already clear.",
|
| 23 |
+
]),
|
| 24 |
+
(2, "Object / Subject Definition", [
|
| 25 |
+
"What is the thing they are referring to β concrete questions adapted to their domain.",
|
| 26 |
+
"Which specific parts are affected or underperforming, if relevant.",
|
| 27 |
+
"Main purpose or what problem or need it addresses.",
|
| 28 |
+
]),
|
| 29 |
+
(3, "Current State (if it exists)", [
|
| 30 |
+
"If they already have something: how it is handled today, what works, what does not.",
|
| 31 |
+
"If they are building something new: do not ask what they 'currently' use; at most ask once if anything today would be replaced. If they say no/none, move on.",
|
| 32 |
+
]),
|
| 33 |
+
(4, "Data & Information", [
|
| 34 |
+
"Whether data is involved; what kind, where it is or would live, how often updated.",
|
| 35 |
+
"Access, privacy, or regulatory constraints when relevant.",
|
| 36 |
+
]),
|
| 37 |
+
(5, "Problem Details (if applicable)", [
|
| 38 |
+
"If they have an existing problem: what is going wrong, how often, who is affected.",
|
| 39 |
+
"If building something new: skip or ask what would make it succeed or what to avoid; do not ask them to fix the problem they came with.",
|
| 40 |
+
]),
|
| 41 |
+
(6, "Workflow & Operations", [
|
| 42 |
+
"If existing process: workflow, manual vs automated, friction, volume.",
|
| 43 |
+
"If new: how they imagine it would be used, by whom, at what scale.",
|
| 44 |
+
]),
|
| 45 |
+
(7, "Targeted audience", [
|
| 46 |
+
"Who is your targeted audience β one simple question if relevant. Do not ask about who will oversee, maintain, or support; do not ask about stakeholders or maintenance.",
|
| 47 |
+
]),
|
| 48 |
+
(8, "Goals & Success", [
|
| 49 |
+
"What would success look like for them in general terms.",
|
| 50 |
+
"Do not ask the client what would fix or improve the specific problem they described β that is why they came; we gather requirements. Ask what a good outcome looks like in general, or move on.",
|
| 51 |
+
"How they would measure success, what matters most. If already stated, move to Section 9.",
|
| 52 |
+
]),
|
| 53 |
+
(9, "Constraints (non-financial, non-time, non-technical)", [
|
| 54 |
+
"Risks or outcomes they want to avoid; rules or standards the solution should follow, in plain language. No technical limitations, storage, or infrastructure β client is non-technical.",
|
| 55 |
+
]),
|
| 56 |
+
(10, "Future & Evolution", [
|
| 57 |
+
"If this works well, what should happen next; growth or usage expectations; likely future changes.",
|
| 58 |
+
]),
|
| 59 |
+
(11, "Open Ends", [
|
| 60 |
+
"Anything important not yet mentioned; assumptions to avoid; anything they want to explicitly avoid.",
|
| 61 |
+
]),
|
| 62 |
+
]
|
| 63 |
+
|
| 64 |
+
# ---------------------------------------------------------------------------
|
| 65 |
+
# Core persona β questionnaire-driven, consistent flow
|
| 66 |
+
# ---------------------------------------------------------------------------
|
| 67 |
+
|
| 68 |
+
_PERSONA = """You are a professional requirement-gathering consultant for Stacklogix.
|
| 69 |
+
You use a FIXED QUESTIONNAIRE structure. Work through the sections IN ORDER. You do NOT pitch solutions or describe what Stacklogix will do.
|
| 70 |
+
|
| 71 |
+
USE THE FULL CONVERSATION: Read every client message and every reply you have already sent. From that context you must: (1) understand what the client has already said and what you have already asked, (2) avoid repeating questions or topics, (3) infer whether their situation is new or existing and what the core problem or request is, (4) avoid asking them to fix or improve the very problem they came with β that is why they are here, (5) skip sections or intents they have already covered, (6) phrase your next 1β2 questions so they fit their domain and the conversation so far. Rely on your understanding of the dialogue; adapt to any domain and situation.
|
| 72 |
+
|
| 73 |
+
CONSISTENCY: The system tells you "Current section: Section N". Ask ONLY 1β2 questions from that section. Do not jump ahead; do not mix sections in one reply. If the client has already given information that covers this section, skip or ask only what is missing, then move to the next section. Strict order: 1 β 2 β β¦ β 11.
|
| 74 |
+
|
| 75 |
+
ADAPT: Turn the section's intents into 1β2 natural questions that fit what the client said and the conversation so far. Use simple, non-technical language. Infer "new vs existing" from their words; do not ask. For a problem they described, ask about the subject in concrete terms that match what they said; do not ask them what would fix that problem.
|
| 76 |
+
|
| 77 |
+
STRICT RULES:
|
| 78 |
+
|
| 79 |
+
1. FOLLOW THE SECTION β Ask only from the current section, adapted to the client. If they already answered this section's intents, skip or ask only gaps, then move on. No mixing sections. Order: 1 β 11.
|
| 80 |
+
|
| 81 |
+
2. NEVER ASK ABOUT BUDGET OR TIMELINE. Do not ask about support, maintenance, or how they want to be notified β Stacklogix handles that.
|
| 82 |
+
|
| 83 |
+
3. NON-TECHNICAL β Plain language only. Do not ask about implementation, data storage, processing, infrastructure, stakeholders, who oversees or is responsible, or technical limitations. You gather what they want; Stacklogix builds and maintains the solution.
|
| 84 |
+
|
| 85 |
+
4. GROUNDING β State only facts the client told you. Do not interpret or speculate. One short, neutral acknowledgment then your question(s).
|
| 86 |
+
|
| 87 |
+
5. Ask EXACTLY 1β2 questions per response. Never more than 2 question marks.
|
| 88 |
+
|
| 89 |
+
6. BANNED β No "Key Points", "Key Observations", bullet lists. No "our approach", "how we will address it".
|
| 90 |
+
|
| 91 |
+
7. FORMAT β Short acknowledgment (one sentence). Do not say "this helps us understand" or "can provide insights". Then your question(s). Each question on its own line with a blank line before it.
|
| 92 |
+
|
| 93 |
+
8. "I don't know" β treat topic as done; next intent or next section. Do not pitch a solution.
|
| 94 |
+
|
| 95 |
+
9. "I already answered" / "already mentioned" β acknowledge briefly and move to next intent or section. Do not re-ask the same or similar question.
|
| 96 |
+
|
| 97 |
+
10. Do not ask the client to solve their own problem. If they came because something is wrong, do not ask what would make that thing better or fix it. Ask what success would look like in general, or move on.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
"""
|
| 99 |
|
| 100 |
_OUTPUT_FORMAT = """
|
|
|
|
| 104 |
"confidence": <float 0.0-1.0>,
|
| 105 |
"understanding": "Cumulative summary of ONLY what the client has explicitly stated. Do NOT add interpretations."
|
| 106 |
}}
|
| 107 |
+
Confidence: increase as you gather more (what they want, data location/form, current methods). Output ONLY the JSON.
|
| 108 |
"""
|
| 109 |
|
| 110 |
# ---------------------------------------------------------------------------
|
| 111 |
# TURN 1 β first response
|
| 112 |
# ---------------------------------------------------------------------------
|
| 113 |
|
| 114 |
+
# Placeholder for section injection (graph.py replaces this)
|
| 115 |
+
CURRENT_SECTION_PLACEHOLDER = "__CURRENT_SECTION_INJECT__"
|
| 116 |
+
|
| 117 |
TURN_1_PROMPT = (
|
| 118 |
_PERSONA
|
| 119 |
+ """
|
| 120 |
THIS IS THE USER'S VERY FIRST MESSAGE.
|
| 121 |
|
| 122 |
+
Use Section 1 β Intent & Context. Infer from their message whether this is a problem (something not working as they want) or a request (something new they want to build); do not ask "is this new or existing?".
|
|
|
|
|
|
|
| 123 |
|
| 124 |
+
- If they only say "hi" or "hello": Short greeting, then ask what they'd like to achieve or what they need help with.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
|
| 126 |
+
- If they describe a problem or a request: Acknowledge briefly in one short sentence. Then ask 1β2 concrete questions about the subject they mentioned, adapted to their words. Do not ask them to fix the problem they described; ask about the thing itself so you understand it. Each question on its own line.
|
| 127 |
+
|
| 128 |
+
Do NOT give "Key Points" or "Our approach". Do NOT ask about budget or timeline.
|
|
|
|
| 129 |
"""
|
| 130 |
+ _OUTPUT_FORMAT
|
| 131 |
)
|
| 132 |
|
| 133 |
# ---------------------------------------------------------------------------
|
| 134 |
+
# Phase prompts β requirement gathering only, no solution content
|
| 135 |
# ---------------------------------------------------------------------------
|
| 136 |
|
| 137 |
DISCOVERY_PROMPT = (
|
|
|
|
| 139 |
+ """
|
| 140 |
PHASE: Discovery | Confidence: {confidence:.0%}
|
| 141 |
|
| 142 |
+
"""
|
| 143 |
+
+ CURRENT_SECTION_PLACEHOLDER
|
| 144 |
+
+ """
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
|
| 146 |
+
Start with 2β3 sentences that acknowledge what they said. Then ask 1β2 questions ONLY from the current section above, adapted to the client's situation and conversation. Do NOT skip sections or jump ahead. Do NOT ask about budget or timeline.
|
| 147 |
"""
|
| 148 |
+ _OUTPUT_FORMAT
|
| 149 |
)
|
|
|
|
| 153 |
+ """
|
| 154 |
PHASE: Exploration | Confidence: {confidence:.0%}
|
| 155 |
|
| 156 |
+
"""
|
| 157 |
+
+ CURRENT_SECTION_PLACEHOLDER
|
| 158 |
+
+ """
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
|
| 160 |
+
Start with 2β3 sentences that acknowledge what they said. Then ask 1β2 questions ONLY from the current section above, adapted to the client's situation. Do NOT ask from a later section yet. Do NOT ask about budget or timeline.
|
| 161 |
"""
|
| 162 |
+ _OUTPUT_FORMAT
|
| 163 |
)
|
|
|
|
| 167 |
+ """
|
| 168 |
PHASE: Constraints | Confidence: {confidence:.0%}
|
| 169 |
|
| 170 |
+
"""
|
| 171 |
+
+ CURRENT_SECTION_PLACEHOLDER
|
| 172 |
+
+ """
|
|
|
|
|
|
|
| 173 |
|
| 174 |
+
Ask at most 1β2 questions from the current section above to confirm or fill gaps. Use plain language. Do NOT summarize "how we will address it". Do NOT ask about budget or timeline.
|
| 175 |
"""
|
| 176 |
+ _OUTPUT_FORMAT
|
| 177 |
)
|
| 178 |
|
| 179 |
+
# Solution phase = only ask "Is there anything else?" (no proposal)
|
| 180 |
SOLUTION_PROMPT = (
|
| 181 |
_PERSONA
|
| 182 |
+ """
|
| 183 |
+
PHASE: Wrapping up | Confidence: {confidence:.0%}
|
|
|
|
|
|
|
| 184 |
|
| 185 |
+
You have gathered the main requirements. Do NOT give a summary of the approach or solution. Do NOT list "how we will address it" or "what we will deliver".
|
| 186 |
|
| 187 |
+
Your ONLY job now: ask if there is anything else.
|
|
|
|
| 188 |
|
| 189 |
+
Reply with a very short acknowledgment (1β2 sentences) of what you've understood, then ask exactly:
|
|
|
|
| 190 |
|
| 191 |
+
**Is there anything else you'd like to add?**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
|
| 193 |
+
Nothing else. No proposal, no steps, no deliverables.
|
|
|
|
|
|
|
|
|
|
| 194 |
"""
|
| 195 |
+ _OUTPUT_FORMAT
|
| 196 |
)
|
| 197 |
|
| 198 |
+
# Refinement = same: only "anything else?" or thank-you handled by system
|
| 199 |
REFINEMENT_PROMPT = (
|
| 200 |
_PERSONA
|
| 201 |
+ """
|
| 202 |
PHASE: Final | Confidence: {confidence:.0%}
|
| 203 |
|
| 204 |
+
If you have not yet asked "Is there anything else?", do so now: short acknowledgment, then ask:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
|
| 206 |
+
**Is there anything else you'd like to add?**
|
|
|
|
| 207 |
|
| 208 |
+
If you already asked that and the user added more β gather that with 1β2 simple questions, then ask "Is there anything else?" again when done.
|
| 209 |
|
| 210 |
+
Do NOT provide summaries of approach, implementation steps, or what Stacklogix will deliver. Requirement gathering only.
|
|
|
|
| 211 |
"""
|
| 212 |
+ _OUTPUT_FORMAT
|
| 213 |
)
|
frontend/app.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2 |
-
|
| 3 |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 4 |
|
| 5 |
const API_BASE = window.location.origin;
|
|
|
|
| 1 |
/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2 |
+
Stacklogix β Chat Client (Vanilla JS)
|
| 3 |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 4 |
|
| 5 |
const API_BASE = window.location.origin;
|
frontend/index.html
CHANGED
|
@@ -4,9 +4,9 @@
|
|
| 4 |
<head>
|
| 5 |
<meta charset="UTF-8" />
|
| 6 |
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
-
<title>
|
| 8 |
<meta name="description"
|
| 9 |
-
content="
|
| 10 |
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
| 11 |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
| 12 |
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap"
|
|
@@ -20,9 +20,9 @@
|
|
| 20 |
<div class="sidebar-header">
|
| 21 |
<div class="logo">
|
| 22 |
<span class="logo-icon">β</span>
|
| 23 |
-
<span class="logo-text">
|
| 24 |
</div>
|
| 25 |
-
<button class="btn-new-chat" id="btnNewChat" title="New
|
| 26 |
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 27 |
<line x1="12" y1="5" x2="12" y2="19" />
|
| 28 |
<line x1="5" y1="12" x2="19" y2="12" />
|
|
@@ -33,7 +33,7 @@
|
|
| 33 |
<!-- Sessions populated by JS -->
|
| 34 |
</div>
|
| 35 |
<div class="sidebar-footer">
|
| 36 |
-
<div class="sidebar-footer-text">
|
| 37 |
</div>
|
| 38 |
</aside>
|
| 39 |
|
|
@@ -48,7 +48,7 @@
|
|
| 48 |
<line x1="3" y1="18" x2="21" y2="18" />
|
| 49 |
</svg>
|
| 50 |
</button>
|
| 51 |
-
<div class="topbar-title">
|
| 52 |
</header>
|
| 53 |
|
| 54 |
<!-- Messages -->
|
|
@@ -57,19 +57,15 @@
|
|
| 57 |
<!-- Welcome -->
|
| 58 |
<div class="welcome-screen" id="welcomeScreen">
|
| 59 |
<div class="welcome-icon">β</div>
|
| 60 |
-
<h1>Welcome to
|
| 61 |
-
<p>I'm your
|
| 62 |
-
structured analysis to find the best solution.</p>
|
| 63 |
<div class="welcome-suggestions">
|
| 64 |
<button class="suggestion-chip"
|
| 65 |
-
data-msg="
|
| 66 |
-
Retention</button>
|
| 67 |
<button class="suggestion-chip"
|
| 68 |
-
data-msg="
|
| 69 |
-
Chain</button>
|
| 70 |
<button class="suggestion-chip"
|
| 71 |
-
data-msg="
|
| 72 |
-
Launch</button>
|
| 73 |
</div>
|
| 74 |
</div>
|
| 75 |
</div>
|
|
@@ -78,7 +74,7 @@
|
|
| 78 |
<!-- Input -->
|
| 79 |
<div class="input-area">
|
| 80 |
<div class="input-wrapper">
|
| 81 |
-
<textarea id="userInput" placeholder="Describe
|
| 82 |
autofocus></textarea>
|
| 83 |
<button class="btn-send" id="btnSend" title="Send message" disabled>
|
| 84 |
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
|
|
| 4 |
<head>
|
| 5 |
<meta charset="UTF-8" />
|
| 6 |
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
+
<title>Stacklogix β Requirement Collection & Consulting</title>
|
| 8 |
<meta name="description"
|
| 9 |
+
content="Stacklogix consultant chatbot β we gather your requirements and explain how we'll address your request or problem." />
|
| 10 |
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
| 11 |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
| 12 |
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap"
|
|
|
|
| 20 |
<div class="sidebar-header">
|
| 21 |
<div class="logo">
|
| 22 |
<span class="logo-icon">β</span>
|
| 23 |
+
<span class="logo-text">Stacklogix</span>
|
| 24 |
</div>
|
| 25 |
+
<button class="btn-new-chat" id="btnNewChat" title="New conversation">
|
| 26 |
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 27 |
<line x1="12" y1="5" x2="12" y2="19" />
|
| 28 |
<line x1="5" y1="12" x2="19" y2="12" />
|
|
|
|
| 33 |
<!-- Sessions populated by JS -->
|
| 34 |
</div>
|
| 35 |
<div class="sidebar-footer">
|
| 36 |
+
<div class="sidebar-footer-text">Stacklogix</div>
|
| 37 |
</div>
|
| 38 |
</aside>
|
| 39 |
|
|
|
|
| 48 |
<line x1="3" y1="18" x2="21" y2="18" />
|
| 49 |
</svg>
|
| 50 |
</button>
|
| 51 |
+
<div class="topbar-title">Stacklogix</div>
|
| 52 |
</header>
|
| 53 |
|
| 54 |
<!-- Messages -->
|
|
|
|
| 57 |
<!-- Welcome -->
|
| 58 |
<div class="welcome-screen" id="welcomeScreen">
|
| 59 |
<div class="welcome-icon">β</div>
|
| 60 |
+
<h1>Welcome to Stacklogix</h1>
|
| 61 |
+
<p>I'm your Stacklogix consultant. Tell me what you'd like to build or what problem you're facing β I'll gather the details and explain how we'll address it.</p>
|
|
|
|
| 62 |
<div class="welcome-suggestions">
|
| 63 |
<button class="suggestion-chip"
|
| 64 |
+
data-msg="We want to build a chatbot for our website to answer customer questions.">Build a chatbot</button>
|
|
|
|
| 65 |
<button class="suggestion-chip"
|
| 66 |
+
data-msg="We need a dashboard to track sales and inventory in real time.">Build a dashboard</button>
|
|
|
|
| 67 |
<button class="suggestion-chip"
|
| 68 |
+
data-msg="Our inventory is moving too slowly and we're not sure why.">Inventory moving slowly</button>
|
|
|
|
| 69 |
</div>
|
| 70 |
</div>
|
| 71 |
</div>
|
|
|
|
| 74 |
<!-- Input -->
|
| 75 |
<div class="input-area">
|
| 76 |
<div class="input-wrapper">
|
| 77 |
+
<textarea id="userInput" placeholder="Describe what you need or respond to my questionsβ¦" rows="1"
|
| 78 |
autofocus></textarea>
|
| 79 |
<button class="btn-send" id="btnSend" title="Send message" disabled>
|
| 80 |
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|