Spaces:
Running
Running
File size: 55,960 Bytes
31add3b 5dd6b0d 31add3b |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 |
# InsightFlow AI - Main Application
import chainlit as cl
from insight_state import InsightFlowState # Import InsightFlowState
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from openai import AsyncOpenAI # Import AsyncOpenAI for DALL-E
from langgraph.graph import StateGraph, END
from langchain_core.messages import HumanMessage, SystemMessage # Added for prompts
from utils.persona import PersonaFactory # Import PersonaFactory
from utils.visualization_utils import generate_dalle_image, generate_mermaid_code # <--- UPDATED IMPORT
import asyncio # For asyncio.gather in execute_persona_tasks
from langchain_core.callbacks.base import AsyncCallbackHandler # <--- ADDED for progress
from typing import Any, Dict, List, Optional, Union # <--- ADDED for callbacks
from langchain_core.outputs import LLMResult # <--- ADDED for callbacks
import datetime # For export filenames
from pathlib import Path # For export directory
from chainlit.input_widget import Switch, Select # <--- REMOVE Option import
# from chainlit.input_widget import Collapse # <--- COMMENT OUT FOR NOW
# from chainlit.element import Divider # <--- REMOVE Collapse from this import for now
# --- RAG UTILS IMPORT ---
from utils.rag_utils import get_embedding_model_instance, get_relevant_context_for_query
# --- END RAG UTILS IMPORT ---
# --- GLOBAL CONFIGURATION STATE ---
_configurations_initialized = False
# _embedding_model_initialized = False # REMOVE OLD GLOBAL FLAG
llm_planner = None
llm_synthesizer = None
llm_direct = None
llm_analytical = None
llm_scientific = None
llm_philosophical = None
llm_factual = None
llm_metaphorical = None
llm_futuristic = None
llm_mermaid_generator = None # <--- ADDED
openai_async_client = None # For DALL-E
PERSONA_LLM_MAP = {}
# Embedding Model Identifiers
OPENAI_EMBED_MODEL_ID = "text-embedding-3-small"
# IMPORTANT: Replace with your actual fine-tuned model ID from Hugging Face Hub after training
FINETUNED_BALANCED_TEAM_EMBED_ID = "suh4s/insightflow-balanced-team-embed-v1-7099e82c-e4c8-48ed-88a8-36bd9255036b" # Placeholder
QUICK_MODE_PERSONAS = ["analytical", "factual"] # Default personas for Quick Mode
# --- RAG Configuration (moved to global scope) ---
RAG_ENABLED_PERSONA_IDS = ["analytical", "philosophical", "metaphorical"]
# --- End RAG Configuration ---
PERSONA_TEAMS = {
"creative_synthesis": {
"name": "π¨ Creative Synthesis Team",
"description": "Generates novel ideas and artistic interpretations.",
"members": ["metaphorical", "futuristic", "philosophical"]
},
"data_driven_analysis": {
"name": "π Data-Driven Analysis Squad",
"description": "Focuses on factual accuracy and logical deduction.",
"members": ["analytical", "factual", "scientific"]
},
"balanced_overview": {
"name": "βοΈ Balanced Overview Group",
"description": "Provides a well-rounded perspective.",
"members": ["analytical", "philosophical", "metaphorical"] # UPDATED: factual -> metaphorical
}
}
def initialize_configurations():
"""Loads environment variables and initializes LLM configurations."""
global _configurations_initialized
global llm_planner, llm_synthesizer, llm_direct, llm_analytical, llm_scientific
global llm_philosophical, llm_factual, llm_metaphorical, llm_futuristic
global llm_mermaid_generator # <--- ADDED
global openai_async_client # Add new client to globals
global PERSONA_LLM_MAP
if _configurations_initialized:
return
print("Initializing configurations: Loading .env and setting up LLMs...")
load_dotenv()
# LLM CONFIGURATIONS
llm_planner = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.1)
llm_synthesizer = ChatOpenAI(model="gpt-4o-mini", temperature=0.4)
llm_direct = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.3)
llm_analytical = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.2)
llm_scientific = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.3)
llm_philosophical = ChatOpenAI(model="gpt-4o-mini", temperature=0.5)
llm_factual = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.1)
llm_metaphorical = ChatOpenAI(model="gpt-4o-mini", temperature=0.6)
llm_futuristic = ChatOpenAI(model="gpt-4o-mini", temperature=0.6)
llm_mermaid_generator = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.1) # <--- ADDED INITIALIZATION
# Initialize OpenAI client for DALL-E etc.
openai_async_client = AsyncOpenAI()
# Mapping persona IDs to their specific LLM instances
PERSONA_LLM_MAP.update({
"analytical": llm_analytical,
"scientific": llm_scientific,
"philosophical": llm_philosophical,
"factual": llm_factual,
"metaphorical": llm_metaphorical,
"futuristic": llm_futuristic,
})
_configurations_initialized = True
print("Configurations initialized.")
# Load environment variables first
# load_dotenv() # Moved to initialize_configurations
# --- LLM CONFIGURATIONS ---
# Configurations based on tests/test_llm_config.py
# llm_planner = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.1) # Moved
# llm_synthesizer = ChatOpenAI(model="gpt-4o-mini", temperature=0.4) # Moved
# llm_direct = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.3) # Moved
# llm_analytical = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.2) # Moved
# llm_scientific = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.3) # Moved
# llm_philosophical = ChatOpenAI(model="gpt-4o-mini", temperature=0.5) # Moved
# llm_factual = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.1) # Moved
# llm_metaphorical = ChatOpenAI(model="gpt-4o-mini", temperature=0.6) # Moved
# llm_futuristic = ChatOpenAI(model="gpt-4o-mini", temperature=0.6) # Moved
# Mapping persona IDs to their specific LLM instances
# PERSONA_LLM_MAP = { # Moved and will be populated in initialize_configurations
# "analytical": llm_analytical,
# "scientific": llm_scientific,
# "philosophical": llm_philosophical,
# "factual": llm_factual,
# "metaphorical": llm_metaphorical,
# "futuristic": llm_futuristic,
# Add other personas here if they have dedicated LLMs or share one from above
# }
# --- SYSTEM PROMPTS (from original app.py) ---
DIRECT_SYSPROMPT = """You are a highly intelligent AI assistant that provides clear, direct, and helpful answers.
Your responses should be accurate, concise, and well-reasoned."""
SYNTHESIZER_SYSTEM_PROMPT_TEMPLATE = """You are a master synthesizer AI. Your task is to integrate the following diverse perspectives into a single, coherent, and insightful response. Ensure that the final synthesis is well-structured, easy to understand, and accurately reflects the nuances of each provided viewpoint. Do not simply list the perspectives; weave them together.
Perspectives:
{formatted_perspectives}
Synthesized Response:"""
# --- LANGGRAPH NODE FUNCTIONS (DUMMIES FOR NOW) ---
async def run_planner_agent(state: InsightFlowState) -> InsightFlowState:
print(f"Node: run_planner_agent, Query: {state.get('query')}")
progress_msg = cl.user_session.get("progress_msg")
completed_steps_log = cl.user_session.get("completed_steps_log")
activity_description = "Planning research approach..."
activity_emoji = "π
"
current_activity_display = f"{activity_emoji} {activity_description} (10%)"
if progress_msg:
progress_msg.content = f"**Current Activity:**\n{current_activity_display}"
if completed_steps_log:
progress_msg.content = f"**Completed Steps:**\n{(chr(10)).join(completed_steps_log)}\n\n{progress_msg.content}"
await progress_msg.update()
completed_steps_log.append(f"{activity_emoji} {activity_description}") # Log without percentage
cl.user_session.set("completed_steps_log", completed_steps_log)
state["current_step_name"] = "execute_persona_tasks"
return state
async def execute_persona_tasks(state: InsightFlowState) -> InsightFlowState:
print(f"Node: execute_persona_tasks")
progress_msg = cl.user_session.get("progress_msg")
completed_steps_log = cl.user_session.get("completed_steps_log", [])
activity_description = "Generating perspectives from selected team..."
activity_emoji = "π§ "
current_activity_display = f"{activity_emoji} {activity_description} (20%)"
if progress_msg:
progress_msg.content = f"**Completed Steps:**\n{(chr(10)).join(completed_steps_log)}\n\n**Current Activity:**\n{current_activity_display}"
await progress_msg.update()
persona_factory: PersonaFactory = cl.user_session.get("persona_factory")
selected_persona_ids = state.get("selected_personas", [])
query = state.get("query")
if not selected_persona_ids:
state["persona_responses"] = {"error": "No personas selected for perspective generation."}
state["current_step_name"] = "synthesize_responses" # Or an error handling state
if progress_msg: completed_steps_log.append(f"{activity_emoji} {activity_description} - No personas selected.")
cl.user_session.set("completed_steps_log", completed_steps_log)
return state
await cl.Message(content=f"Invoking {len(selected_persona_ids)} personas...").send()
tasks = []
# --- RAG Enhancement ---
embedding_model = cl.user_session.get("embedding_model_instance") # <--- GET MODEL FROM SESSION
global_rag_enabled = cl.user_session.get("enable_rag", True)
# --- End RAG Enhancement ---
valid_persona_ids_for_results = [] # Keep track of personas for which tasks were created
for persona_id in selected_persona_ids:
persona_llm = PERSONA_LLM_MAP.get(persona_id.lower())
if not persona_llm:
print(f"Warning: LLM not found for persona {persona_id}. Skipping.")
continue
persona = persona_factory.create_persona(persona_id, persona_llm)
if persona:
final_query_for_llm = query # Default to original query
# --- RAG Integration for Balanced Team Personas ---
if global_rag_enabled and persona.persona_id.lower() in RAG_ENABLED_PERSONA_IDS: # Check global toggle first; Changed persona.id to persona.persona_id
if embedding_model:
rag_progress_message = f"\n π Persona '{persona.name}' (RAG enabled): Searching knowledge base for context related to: '{query[:50]}...'"
if progress_msg: await progress_msg.stream_token(rag_progress_message)
print(rag_progress_message.strip())
retrieved_context = await get_relevant_context_for_query(query, persona.persona_id, embedding_model)
if retrieved_context:
context_log_msg = f" β
Context retrieved for '{persona.name}'. Augmenting prompt."
# print(f"Retrieved context for {persona.id}:\n{retrieved_context}") # Full context - verbose
final_query_for_llm = f"""As the {persona.name}, consider the following retrieved context to answer the user's query.
Integrate this context with your inherent expertise and perspective.
Retrieved Context:
---
{retrieved_context}
---
User Query: {query}
Answer as {persona.name}:
"""
if progress_msg: await progress_msg.stream_token(f"\n π‘ Context found for {persona.name}. Crafting response...")
print(context_log_msg)
else:
no_context_log_msg = f" βΉοΈ No specific context found for '{persona.name}' for this query. Relying on general knowledge."
if progress_msg: await progress_msg.stream_token(f"\n π§ No specific context found for {persona.name}. Proceeding with general knowledge...")
print(no_context_log_msg)
# Prompt when no context is found - persona relies on its base system prompt and expertise
# The persona.generate_perspective method will use its inherent system_prompt.
# We can just pass the original query, or a slightly modified one indicating no specific context was found.
final_query_for_llm = f"""As the {persona.name}, answer the user's query using your inherent expertise and perspective.
No specific context from the knowledge base was retrieved for this particular query.
User Query: {query}
Answer as {persona.name}:
"""
else:
no_embed_msg = f"\n β οΈ Embedding model (current selection) not available for RAG for persona '{persona.name}'. Using general knowledge."
if progress_msg: await progress_msg.stream_token(no_embed_msg)
print(no_embed_msg.strip())
# --- End RAG Integration ---
# Stream persona-specific LLM call message
llm_call_msg = f"\n π£οΈ Consulting AI for {persona.name} perspective..."
if progress_msg: await progress_msg.stream_token(llm_call_msg)
print(llm_call_msg.strip())
tasks.append(persona.generate_perspective(final_query_for_llm)) # Use final_query_for_llm
valid_persona_ids_for_results.append(persona_id)
else:
print(f"Warning: Could not create persona object for {persona_id}. Skipping.")
state["persona_responses"] = {} # Initialize/clear previous responses
if tasks:
try:
# Timeout logic can be added here if needed for asyncio.gather
if progress_msg: await progress_msg.stream_token(f"\n βͺ Invoking {len(valid_persona_ids_for_results)} personas...")
persona_results = await asyncio.gather(*tasks)
# Store responses keyed by the valid persona_ids used for tasks
for i, persona_id in enumerate(valid_persona_ids_for_results):
state["persona_responses"][persona_id] = persona_results[i]
except Exception as e:
print(f"Error during persona perspective generation: {e}")
state["error_message"] = f"Error generating perspectives: {str(e)[:100]}"
# Optionally, populate partial results if some tasks succeeded before error
# For now, just reports error. Individual task errors could be handled in PersonaReasoning too.
if progress_msg:
completed_activity_description = "Perspectives generated."
# Emoji for this completed step was π§ from current_activity_display
completed_steps_log.append(f"{activity_emoji} {completed_activity_description}")
cl.user_session.set("completed_steps_log", completed_steps_log)
# Display updated completed list; the next node will set the new "Current Activity"
# For this brief moment, show only completed steps before next node updates with its current activity
progress_msg.content = f"**Completed Steps:**\n{(chr(10)).join(completed_steps_log)}\n\n**Current Activity:**\nβ
Perspectives generated. (60%)" # Show this as current before next node takes over
await progress_msg.update()
state["current_step_name"] = "synthesize_responses"
return state
async def synthesize_responses(state: InsightFlowState) -> InsightFlowState:
print(f"Node: synthesize_responses")
progress_msg = cl.user_session.get("progress_msg")
completed_steps_log = cl.user_session.get("completed_steps_log")
activity_description = "Synthesizing insights..."
activity_emoji = "βοΈ"
current_activity_display = f"{activity_emoji} {activity_description} (65%)"
if progress_msg:
progress_msg.content = f"**Current Activity:**\n{current_activity_display}"
if completed_steps_log:
progress_msg.content = f"**Completed Steps:**\n{(chr(10)).join(completed_steps_log)}\n\n{progress_msg.content}"
await progress_msg.update()
persona_responses = state.get("persona_responses", {})
if not persona_responses:
print("No persona responses to synthesize.")
state["synthesized_response"] = "No perspectives were available to synthesize."
state["current_step_name"] = "generate_visualization"
return state
formatted_perspectives_list = []
for persona_id, response_text in persona_responses.items():
formatted_perspectives_list.append(f"- Perspective from {persona_id}: {response_text}")
formatted_perspectives_string = "\n".join(formatted_perspectives_list)
final_prompt_content = SYNTHESIZER_SYSTEM_PROMPT_TEMPLATE.format(
formatted_perspectives=formatted_perspectives_string
)
messages = [
SystemMessage(content=final_prompt_content)
]
try:
# Ensure llm_synthesizer is available (initialized by initialize_configurations)
if llm_synthesizer is None:
print("Error: llm_synthesizer is not initialized.")
state["error_message"] = "Synthesizer LLM not available."
state["synthesized_response"] = "Synthesis failed due to internal error."
state["current_step_name"] = "error_presenting" # Or a suitable error state
return state
ai_response = await llm_synthesizer.ainvoke(messages)
synthesized_text = ai_response.content
state["synthesized_response"] = synthesized_text
print(f"Synthesized response: {synthesized_text[:200]}...") # Log snippet
except Exception as e:
print(f"Error during synthesis: {e}")
state["error_message"] = f"Synthesis error: {str(e)[:100]}"
state["synthesized_response"] = "Synthesis failed."
# Optionally, decide if we proceed to visualization or an error state
# For now, let's assume we still try to visualize if there's a partial/failed synthesis
if progress_msg:
completed_activity_description = "Insights synthesized."
completed_steps_log.append(f"{activity_emoji} {completed_activity_description}")
cl.user_session.set("completed_steps_log", completed_steps_log)
progress_msg.content = f"**Completed Steps:**\n{(chr(10)).join(completed_steps_log)}\n\n**Current Activity:**\nβ
Insights synthesized. (80%)"
await progress_msg.update()
state["current_step_name"] = "generate_visualization"
return state
async def generate_visualization(state: InsightFlowState) -> InsightFlowState:
print(f"Node: generate_visualization")
progress_msg = cl.user_session.get("progress_msg")
completed_steps_log = cl.user_session.get("completed_steps_log")
activity_description = "Creating visualizations..."
activity_emoji = "π¨"
current_activity_display = f"{activity_emoji} {activity_description} (85%)"
if progress_msg:
progress_msg.content = f"**Current Activity:**\n{current_activity_display}"
if completed_steps_log:
progress_msg.content = f"**Completed Steps:**\n{(chr(10)).join(completed_steps_log)}\n\n{progress_msg.content}"
await progress_msg.update()
synthesized_response = state.get("synthesized_response")
image_url = None
mermaid_code_output = None # Changed variable name for clarity
# DALL-E Image Generation (existing logic)
if synthesized_response and openai_async_client:
# Log the full DALL-E prompt for debugging if issues persist
# print(f"Full DALL-E prompt for visualization: {dalle_prompt}")
dalle_prompt = f"A hand-drawn style visual note or sketch representing the key concepts of: {synthesized_response}"
if len(dalle_prompt) > 4000:
dalle_prompt = dalle_prompt[:3997] + "..."
print(f"Attempting DALL-E image generation for: {dalle_prompt[:100]}...")
image_url = await generate_dalle_image(prompt=dalle_prompt, client=openai_async_client)
if image_url:
state["visualization_image_url"] = image_url
print(f"DALL-E Image URL: {image_url}")
else:
print("DALL-E image generation failed or returned no URL.")
state["visualization_image_url"] = None
elif not synthesized_response:
print("No synthesized response available to generate DALL-E image.")
state["visualization_image_url"] = None
elif not openai_async_client:
print("OpenAI async client not initialized, skipping DALL-E generation.")
state["visualization_image_url"] = None
# Mermaid Code Generation
if synthesized_response and llm_mermaid_generator: # Check if both are available
print(f"Attempting Mermaid code generation for: {synthesized_response[:100]}...")
mermaid_code_output = await generate_mermaid_code(synthesized_response, llm_mermaid_generator)
if mermaid_code_output:
state["visualization_code"] = mermaid_code_output
print(f"Mermaid code generated: {mermaid_code_output[:100]}...")
else:
print("Mermaid code generation failed or returned no code.")
state["visualization_code"] = None # Ensure it's None if failed
else:
print("Skipping Mermaid code generation due to missing response or LLM.")
state["visualization_code"] = None
if progress_msg:
completed_activity_description = "Visualizations created."
completed_steps_log.append(f"{activity_emoji} {completed_activity_description}")
cl.user_session.set("completed_steps_log", completed_steps_log)
progress_msg.content = f"**Completed Steps:**\n{(chr(10)).join(completed_steps_log)}\n\n**Current Activity:**\nβ
Visualizations created. (95%)"
await progress_msg.update()
state["current_step_name"] = "present_results"
return state
async def present_results(state: InsightFlowState) -> InsightFlowState:
print(f"Node: present_results")
progress_msg = cl.user_session.get("progress_msg")
completed_steps_log = cl.user_session.get("completed_steps_log")
activity_description = "Preparing final presentation..."
activity_emoji = "π"
current_activity_display = f"{activity_emoji} {activity_description} (98%)"
if progress_msg:
progress_msg.content = f"**Current Activity:**\n{current_activity_display}"
if completed_steps_log:
progress_msg.content = f"**Completed Steps:**\n{(chr(10)).join(completed_steps_log)}\n\n{progress_msg.content}"
await progress_msg.update()
show_visualization = cl.user_session.get("show_visualization", True)
show_perspectives = cl.user_session.get("show_perspectives", True)
synthesized_response = state.get("synthesized_response")
# 1. Send Synthesized Response (always)
if synthesized_response:
await cl.Message(content=synthesized_response, author="Synthesized Insight").send()
else:
await cl.Message(content="No synthesized response was generated.", author="System").send()
# 2. Send Visualizations (if enabled and available)
if show_visualization:
image_url = state.get("visualization_image_url")
if image_url:
image_element = cl.Image(
url=image_url,
name="dalle_visualization",
display="inline",
size="large"
)
# Send image with a title in the content or as a separate message
await cl.Message(content="Visual Summary:", elements=[image_element], author="System").send()
mermaid_code = state.get("visualization_code")
if mermaid_code:
mermaid_element = cl.Text(
content=mermaid_code,
mime_type="text/mermaid",
name="generated_diagram",
display="inline"
)
await cl.Message(content="Concept Map:", elements=[mermaid_element], author="System").send()
# 3. Send Persona Perspectives within cl.Collapse (if enabled and available)
if show_perspectives:
persona_responses = state.get("persona_responses")
if persona_responses:
perspective_elements = []
for persona_id, response_text in persona_responses.items():
if response_text:
# Temporarily revert to sending simple messages for perspectives
await cl.Message(content=f"**Perspective from {persona_id.capitalize()}:**\n{response_text}", author=persona_id.capitalize()).send()
# perspective_elements.append(
# Collapse( # <--- COMMENT OUT USAGE
# label=f"ποΈ Perspective from {persona_id.capitalize()}",
# content=response_text,
# initial_collapsed=True # Start collapsed
# )
# )
# if perspective_elements:
# await cl.Message(
# content="Dive Deeper into Individual Perspectives:",
# elements=perspective_elements,
# author="System"
# ).send()
state["current_step_name"] = "results_presented"
if progress_msg:
# Log the "Preparing presentation" step as completed first
completed_steps_log.append(f"{activity_emoji} {activity_description}")
cl.user_session.set("completed_steps_log", completed_steps_log)
# Show the 100% completion message as current activity initially
final_emoji = "β¨"
final_message_description = "All insights presented!"
current_activity_display = f"{final_emoji} {final_message_description} (100%)"
progress_msg.content = f"**Completed Steps:**\n{(chr(10)).join(completed_steps_log)}\n\n**Current Activity:**\n{current_activity_display}"
await progress_msg.update()
# Now, move the 100% step to completed and finalize the progress message
await cl.sleep(0.5) # Optional short delay for UI to update before final change
completed_steps_log.append(f"{final_emoji} {final_message_description}")
cl.user_session.set("completed_steps_log", completed_steps_log)
# Generate witty remark
query_for_remark = state.get("query", "this topic")
personas_for_remark = state.get("selected_personas", [])
snippet_for_remark = state.get("synthesized_response")
witty_remark = await generate_witty_completion_remark(query_for_remark, personas_for_remark, snippet_for_remark)
progress_msg.content = f"**Completed Steps:**\n{(chr(10)).join(completed_steps_log)}\n\n{witty_remark}"
await progress_msg.update()
# Send a final message that all content is loaded, then actions
await cl.Message(content="All insights presented. You can now export the results.").send()
# Add Export Actions
export_actions = [
cl.Action(name="export_markdown", label="Export to Markdown", value="markdown", description="Export results to a Markdown file.", payload={}),
cl.Action(name="export_pdf", label="Export to PDF", value="pdf", description="Export results to a PDF file.", payload={})
]
await cl.Message(content="", actions=export_actions).send()
return state
# --- LANGGRAPH SETUP ---
insight_graph_builder = StateGraph(InsightFlowState)
# Add nodes
insight_graph_builder.add_node("planner_agent", run_planner_agent)
insight_graph_builder.add_node("execute_persona_tasks", execute_persona_tasks)
insight_graph_builder.add_node("synthesize_responses", synthesize_responses)
insight_graph_builder.add_node("generate_visualization", generate_visualization)
insight_graph_builder.add_node("present_results", present_results)
# Set entry point
insight_graph_builder.set_entry_point("planner_agent")
# Add edges
insight_graph_builder.add_edge("planner_agent", "execute_persona_tasks")
insight_graph_builder.add_edge("execute_persona_tasks", "synthesize_responses")
insight_graph_builder.add_edge("synthesize_responses", "generate_visualization")
insight_graph_builder.add_edge("generate_visualization", "present_results")
insight_graph_builder.add_edge("present_results", END)
# Compile the graph
insight_flow_graph = insight_graph_builder.compile()
print("LangGraph setup complete.")
# --- CUSTOM CALLBACK HANDLER FOR PROGRESS UPDATES --- #
class InsightFlowCallbackHandler(AsyncCallbackHandler):
def __init__(self, progress_message: cl.Message):
self.progress_message = progress_message
self.step_counter = 0
async def on_chain_start(
self, serialized: Dict[str, Any], inputs: Dict[str, Any], **kwargs: Any
) -> None:
"""Run when chain starts running. Now simplified as nodes will provide more specific updates."""
# This callback might still be useful for very generic LangGraph level start/stop
# but specific node progress will be handled within the node functions themselves.
# Avoid printing "Unknown chain/node" if possible.
# langgraph_event_name = serialized.get("name") or (serialized.get("id")[-1] if isinstance(serialized.get("id"), list) and serialized.get("id") else None)
# if self.progress_message and langgraph_event_name and not langgraph_event_name.startswith("__"):
# self.step_counter += 1
# await self.progress_message.stream_token(f"\nβ³ Step {self.step_counter}: Processing {langgraph_event_name}...\")
pass # Node functions will now handle more granular progress updates
async def on_llm_start(
self, serialized: Dict[str, Any], prompts: List[str], **kwargs: Any
) -> None:
"""Run when LLM starts running. This will now be more generic or can be removed if node-specific messages are enough."""
# llm_display_name = "LLM" # Sensible default
# if serialized:
# id_list = serialized.get("id")
# if isinstance(id_list, list) and len(id_list) > 0:
# raw_name_part = id_list[0]
# if isinstance(raw_name_part, str):
# if "ChatOpenAI".lower() in raw_name_part.lower():
# llm_display_name = "OpenAI Model"
# elif "LLM" in raw_name_part.upper():
# llm_display_name = raw_name_part
# name_val = serialized.get("name")
# if isinstance(name_val, str) and name_val != "Unknown LLM":
# if llm_display_name == "LLM" or len(name_val) > len(llm_display_name):
# llm_display_name = name_val
# update_text = f"π£οΈ Consulting {llm_display_name}... "
# if self.progress_message:
# # print(f"DEBUG on_llm_start serialized: {serialized}")
# await self.progress_message.stream_token(f"\n L {update_text}")
pass # Specific LLM call messages are now sent from execute_persona_tasks
# We can add on_agent_action, on_tool_start for more granular updates if nodes use agents/tools
# For now, on_chain_start (which LangGraph nodes trigger) and on_llm_start should give good visibility.
async def on_chain_end(self, outputs: Dict[str, Any], **kwargs: Any) -> None:
"""Run when chain ends running."""
# chain_name = kwargs.get("name", "a process") # LangGraph provides name in kwargs for on_chain_end
# if self.progress_message:
# await self.progress_message.stream_token(f"\nCompleted: {chain_name}.\")
pass # Node functions will provide end-of-node context if needed
# This allows the test to import these names from app if needed
__all__ = [
"InsightFlowState", "on_chat_start", "on_message",
"invoke_direct_llm", "invoke_langgraph",
"run_planner_agent", "execute_persona_tasks", "synthesize_responses",
"generate_visualization", "present_results",
"StateGraph", # Expose StateGraph if tests patch app.StateGraph directly
"initialize_configurations", # Expose for testing
"on_export_markdown", "on_export_pdf" # Add new export handlers
]
# --- CHAT SETTINGS HELPER ---
async def _apply_chat_settings_to_state():
"""Reads chat settings and updates session and insight_flow_state."""
chat_settings_values = cl.user_session.get("chat_settings", {})
insight_flow_state: InsightFlowState = cl.user_session.get("insight_flow_state")
persona_factory: PersonaFactory = cl.user_session.get("persona_factory")
if not insight_flow_state or not persona_factory:
# Should not happen if on_chat_start ran correctly
print("Error: State or persona factory not found while applying chat settings.")
return
# Update modes in user_session
cl.user_session.set("direct_mode", chat_settings_values.get("direct_mode", False))
cl.user_session.set("quick_mode", chat_settings_values.get("quick_mode", False))
cl.user_session.set("show_perspectives", chat_settings_values.get("show_perspectives", True))
cl.user_session.set("show_visualization", chat_settings_values.get("show_visualization", True))
cl.user_session.set("enable_rag", chat_settings_values.get("enable_rag", True)) # Read the RAG toggle
# Embedding model toggle
new_use_finetuned_setting = chat_settings_values.get("use_finetuned_embedding", False)
current_use_finetuned_setting = cl.user_session.get("use_finetuned_embedding", False)
cl.user_session.set("use_finetuned_embedding", new_use_finetuned_setting)
if new_use_finetuned_setting != current_use_finetuned_setting:
print("Embedding model setting changed. Re-initializing model.")
await _initialize_embedding_model_in_session() # Re-initialize if toggle changed
# Update selected_personas in insight_flow_state
selected_personas_from_settings = []
available_persona_ids = persona_factory.get_available_personas().keys()
for persona_id in available_persona_ids:
if chat_settings_values.get(f"persona_{persona_id}", False):
selected_personas_from_settings.append(persona_id)
# Check if a team is selected and override individual selections
selected_team_id = chat_settings_values.get("selected_team")
if selected_team_id and selected_team_id != "none":
team_info = PERSONA_TEAMS.get(selected_team_id)
if team_info and "members" in team_info:
selected_personas_from_settings = list(team_info["members"]) # Use team members
print(f"Team '{team_info['name']}' selected, overriding individual personas.")
mutable_state = insight_flow_state.copy()
mutable_state["selected_personas"] = selected_personas_from_settings
cl.user_session.set("insight_flow_state", mutable_state)
print(f"Applied chat settings: Direct Mode: {cl.user_session.get('direct_mode')}, Quick Mode: {cl.user_session.get('quick_mode')}")
print(f"Selected Personas from settings: {selected_personas_from_settings}")
@cl.on_chat_start
async def on_chat_start():
"""Initializes session state, chat settings, and sends a welcome message."""
# global _configurations_initialized # No longer need _embedding_model_initialized here
# The global keyword is only needed if you assign to the global variable in this scope.
# We are only reading _configurations_initialized here.
if not _configurations_initialized:
initialize_configurations()
# Initialize with default embedding model (OpenAI) first
# This sets "use_finetuned_embedding" to False in session if not already there
await _initialize_embedding_model_in_session(force_openai=True)
persona_factory = PersonaFactory()
cl.user_session.set("persona_factory", persona_factory)
# Default selections for personas/teams
default_team_id = "balanced_overview"
default_selected_personas = list(PERSONA_TEAMS[default_team_id]["members"])
# Default UI toggles (excluding embedding toggle, handled by _initialize_embedding_model_in_session)
default_direct_mode = False
default_quick_mode = False
default_show_perspectives = True
default_show_visualization = True
default_enable_rag = True # Default RAG to ON
# default_use_finetuned_embedding is set by _initialize_embedding_model_in_session(force_openai=True)
initial_state = InsightFlowState(
panel_type="research",
query="",
selected_personas=default_selected_personas,
persona_responses={},
synthesized_response=None,
visualization_code=None,
visualization_image_url=None,
current_step_name="awaiting_query",
error_message=None
)
cl.user_session.set("insight_flow_state", initial_state)
cl.user_session.set("direct_mode", default_direct_mode)
cl.user_session.set("quick_mode", default_quick_mode)
cl.user_session.set("show_perspectives", default_show_perspectives)
cl.user_session.set("show_visualization", default_show_visualization)
cl.user_session.set("enable_rag", default_enable_rag)
settings_inputs = []
settings_inputs.append(
Switch(id="enable_rag", label="βοΈ Enable RAG Features (for supported personas)", initial=default_enable_rag)
)
settings_inputs.append(
Switch(id="use_finetuned_embedding",
label="π¬ Use Fine-tuned Embedding (Balanced Team - if available)",
initial=cl.user_session.get("use_finetuned_embedding", False)) # Initial from session
)
team_items = {"-- Select a Team (Optional) --": "none"}
for team_id, team_info in PERSONA_TEAMS.items():
team_items[team_info["name"]] = team_id
settings_inputs.append(
Select(
id="selected_team",
label="π― Persona Team (Overrides individual toggles for processing)", # Corrected quotes
items=team_items,
initial_value=default_team_id # This should be fine as it's a variable
)
)
# settings_inputs.append(cl.Divider()) # Keep commented for now
for persona_id, persona_config in persona_factory.persona_configs.items():
settings_inputs.append(
Switch(
id=f"persona_{persona_id}",
label=persona_config["name"],
initial=persona_id in default_selected_personas
)
)
settings_inputs.extend([
Switch(id="direct_mode", label="π Direct Mode (Quick, single LLM answers)", initial=default_direct_mode),
Switch(id="quick_mode", label="β‘ Quick Mode (Uses max 2 personas)", initial=default_quick_mode),
Switch(id="show_perspectives", label="ποΈ Show Individual Perspectives", initial=default_show_perspectives),
Switch(id="show_visualization", label="π¨ Show Visualizations (DALL-E & Mermaid)", initial=default_show_visualization)
])
await cl.ChatSettings(inputs=settings_inputs).send()
# New Welcome Message
welcome_message_content = "π **Welcome to InsightFlow AI!** Dive deep into any topic with a symphony of AI perspectives. I can analyze, synthesize, and even visualize complex information. Configure your AI team using the βοΈ settings, then ask your question!"
await cl.Message(content=welcome_message_content).send()
cl.user_session.set("progress_msg", None)
# Placeholder for direct LLM invocation logic
async def invoke_direct_llm(query: str):
print(f"invoke_direct_llm called with query: {query}")
messages = [
SystemMessage(content=DIRECT_SYSPROMPT),
HumanMessage(content=query)
]
response_message = cl.Message(content="")
await response_message.send()
async for chunk in llm_direct.astream(messages):
if chunk.content:
await response_message.stream_token(chunk.content)
await response_message.update() # Finalize the streamed message
return "Direct response streamed" # Test expects a return, actual content is streamed
# Placeholder for LangGraph invocation logic
async def invoke_langgraph(query: str, initial_state: InsightFlowState):
print(f"invoke_langgraph called with query: {query}")
cl.user_session.set("completed_steps_log", [])
progress_msg = cl.Message(content="")
await progress_msg.send()
# Initial message only shows current activity, no completed steps yet.
progress_msg.content = "**Current Activity:**\nβ³ Initializing InsightFlow process... (0%)"
await progress_msg.update()
cl.user_session.set("progress_msg", progress_msg)
# Setup callback handler - still useful for LLM calls or other low-level events if desired
callback_handler = InsightFlowCallbackHandler(progress_message=progress_msg)
current_state = initial_state.copy() # Work with a copy
current_state["query"] = query
current_state["current_step_name"] = "planner_agent" # Reset step for new invocation
# Check for Quick Mode and adjust personas if needed
quick_mode_active = cl.user_session.get("quick_mode", False) # Default to False if not set
if quick_mode_active:
print("Quick Mode is ON. Using predefined quick mode personas.")
current_state["selected_personas"] = list(QUICK_MODE_PERSONAS) # Ensure it's a new list copy
# If quick_mode is OFF, selected_personas from initial_state (set by on_chat_start or commands) will be used.
# Prepare config for LangGraph invocation (e.g., for session/thread ID)
# In a Chainlit context, cl.user_session.get("id") can give a thread_id
thread_id = cl.user_session.get("id", "default_thread_id") # Get Chainlit thread_id or a default
config = {
"configurable": {"thread_id": thread_id},
"callbacks": [callback_handler] # Add our callback handler
}
# progress_msg = cl.Message(content="β³ Processing with InsightFlow (0%)...") # OLD TODO
# await progress_msg.send()
# cl.user_session.set("progress_msg", progress_msg)
final_state = await insight_flow_graph.ainvoke(current_state, config=config)
# Final progress update
if progress_msg:
await progress_msg.update() # Ensure final content (100%) is sent and displayed
# The present_results node should handle sending messages.
# invoke_langgraph will return the final state which on_message saves.
return final_state
@cl.on_message
async def on_message(message: cl.Message):
"""Handles incoming user messages and routes based on direct_mode."""
# Apply the latest settings from UI to the session and state
await _apply_chat_settings_to_state()
direct_mode = cl.user_session.get("direct_mode") # Values now reflect chat settings
msg_content_lower = message.content.lower().strip()
# Command handling (can be kept as backups or for power users)
if msg_content_lower == "/direct on":
cl.user_session.set("direct_mode", True)
await cl.Message(content="Direct mode ENABLED.").send()
return # Command processed, no further action
elif msg_content_lower == "/direct off":
cl.user_session.set("direct_mode", False)
await cl.Message(content="Direct mode DISABLED.").send()
return # Command processed, no further action
# Command handling for /show perspectives
elif msg_content_lower == "/show perspectives on":
cl.user_session.set("show_perspectives", True)
await cl.Message(content="Show perspectives ENABLED.").send()
return
elif msg_content_lower == "/show perspectives off":
cl.user_session.set("show_perspectives", False)
await cl.Message(content="Show perspectives DISABLED.").send()
return
# Command handling for /show visualization
elif msg_content_lower == "/show visualization on":
cl.user_session.set("show_visualization", True)
await cl.Message(content="Show visualization ENABLED.").send()
return
elif msg_content_lower == "/show visualization off":
cl.user_session.set("show_visualization", False)
await cl.Message(content="Show visualization DISABLED.").send()
return
# Command handling for /quick_mode
elif msg_content_lower == "/quick_mode on":
cl.user_session.set("quick_mode", True)
await cl.Message(content="Quick mode ENABLED.").send()
return
elif msg_content_lower == "/quick_mode off":
cl.user_session.set("quick_mode", False)
await cl.Message(content="Quick mode DISABLED.").send()
return
elif msg_content_lower == "/help": # <-- ADD /help COMMAND HANDLER
await send_help_message()
return
# If not a /direct command, proceed with existing direct_mode check for LLM calls
if direct_mode: # This direct_mode is now sourced from chat settings via _apply_chat_settings_to_state
await invoke_direct_llm(message.content)
else:
insight_flow_state = cl.user_session.get("insight_flow_state")
if not insight_flow_state:
# Fallback if state isn't somehow initialized
await cl.Message(content="Error: Session state not found. Please restart the chat.").send()
return
# The selected_personas in insight_flow_state have been updated by _apply_chat_settings_to_state
# The quick_mode check within invoke_langgraph will use cl.user_session.get("quick_mode")
# which was also updated by _apply_chat_settings_to_state.
updated_state = await invoke_langgraph(message.content, insight_flow_state)
cl.user_session.set("insight_flow_state", updated_state) # Save updated state
print("app.py initialized with LLMs, on_chat_start, and on_message defined")
# --- NEW EXPORT ACTION STUBS ---
@cl.action_callback("export_markdown")
async def on_export_markdown(action: cl.Action):
# Placeholder for Markdown export logic
await cl.Message(content=f"Markdown export for action '{action.name}' initiated (not fully implemented).").send()
@cl.action_callback("export_pdf")
async def on_export_pdf(action: cl.Action):
# Placeholder for PDF export logic
await cl.Message(content=f"PDF export for action '{action.name}' initiated (not fully implemented).").send()
# --- NEW FUNCTION FOR WITTY COMPLETION REMARKS ---
async def generate_witty_completion_remark(query: str, selected_persona_ids: List[str], synthesized_snippet: Optional[str]) -> str:
if not llm_direct: # Ensure llm_direct is initialized
print("llm_direct not initialized, cannot generate witty remark.")
return "Intellectual journey complete! Bravo! βοΈ" # Fallback
persona_factory: PersonaFactory = cl.user_session.get("persona_factory")
persona_names = []
if persona_factory:
for pid in selected_persona_ids:
config = persona_factory.persona_configs.get(pid)
if config and config.get("name"):
persona_names.append(config["name"])
else:
persona_names.append(pid.capitalize())
else:
persona_names = [pid.capitalize() for pid in selected_persona_ids]
persona_names_str = ", ".join(persona_names) if persona_names else "various"
if not synthesized_snippet: synthesized_snippet = "a fascinating topic"
if len(synthesized_snippet) > 100: synthesized_snippet = synthesized_snippet[:97] + "..."
prompt_template = f"""You are a charming and slightly cheeky AI host, like a talk show host wrapping up a segment.
The user just explored the query: '{query}'
with insights from {persona_names_str} perspectives.
The main takeaway was about: '{synthesized_snippet}'.
Craft a short, witty, and encouraging closing remark (1-2 sentences, max 25 words) to signify the completion.
Example: 'And that, folks, is how you dissect a universe! Until next time, keep those neurons firing!'
Another Example: 'Well, that was a delightful dive into the rabbit hole of {query}! Stay curious!'
Your remark:"""
messages = [SystemMessage(content=prompt_template)]
try:
response = await llm_direct.ainvoke(messages)
remark = response.content.strip()
# Basic filter for overly long or nonsensical remarks if needed, though prompt should guide it
if len(remark) > 150 or len(remark) < 10: # Arbitrary length check
return "And that's a wrap on that fascinating exploration! What's next? βοΈ"
return f"{remark} βοΈ"
except Exception as e:
print(f"Error generating witty remark: {e}")
return "Exploration complete! Well done! βοΈ" # Fallback on error
# --- HELP MESSAGE FUNCTION ---
async def send_help_message():
"""Sends the detailed help message to the user."""
help_text_md = """# Welcome to InsightFlow AI - Your Guide!
InsightFlow AI is designed to help you explore complex topics with depth and clarity by providing multiple AI-driven perspectives, synthesizing them into a coherent understanding, and even offering visual summaries. Think of it as your personal team of AI research assistants!
## What Can InsightFlow AI Do?
* **Multi-Perspective Analysis:** Instead of a single answer, InsightFlow AI engages a team of specialized AI "personas" (e.g., Analytical, Philosophical, Scientific) to examine your query from different angles. This provides a richer, more nuanced understanding.
* **Insight Synthesis:** The individual perspectives are then intelligently combined into a single, comprehensive synthesized response.
* **Visualizations (Optional):**
* **DALL-E Sketches:** Get a conceptual, hand-drawn style visual note summarizing the key ideas.
* **Mermaid Diagrams:** See a concept map illustrating the relationships between your query, the personas, and the synthesized view.
* **RAG (Retrieval Augmented Generation - for supported personas):** For certain personas, the AI can search a dedicated knowledge base of relevant documents to ground its responses in specific information, enhancing accuracy and depth.
* **Flexible Interaction Modes:**
* **Full Multi-Persona Mode:** The default mode, engaging your selected team for in-depth analysis.
* **Direct Mode:** Get a quick, straightforward answer from a single LLM, bypassing the multi-persona/LangGraph system.
* **Quick Mode:** A faster multi-persona analysis using a reduced set of (currently 2) predefined personas.
* **Exportable Results:** You'll be able to export your analysis (though this feature is still under full development).
## Why Does It Work This Way? (The Philosophy)
InsightFlow AI is built on the idea that complex topics are best understood by examining them from multiple viewpoints. Just like a team of human experts can provide more comprehensive insight than a single individual, our AI personas work together to give you a more well-rounded and deeply considered response. The structured workflow (managed by LangGraph) ensures a methodical approach to generating and synthesizing these perspectives.
## Using the Settings (βοΈ Gear Icon)
You can customize your InsightFlow AI experience using the settings panel:
* **βοΈ Enable RAG Features:**
* **Functionality:** (Currently being refined) When enabled, personas designated as "RAG-enabled" (like Analytical, Philosophical, Metaphorical on the Balanced Team) will attempt to search their specific knowledge bases for relevant context before generating their perspective. This can lead to more informed and detailed answers.
* **Status:** Basic RAG data loading and vector store creation for personas is active. The quality and breadth of data sources are still being expanded.
* **π― Persona Team:**
* **Functionality:** Allows you to quickly select a pre-configured team of personas. Selecting a team will override any individual persona toggles below.
* **Current Teams:**
* `π¨ Creative Synthesis Team`: (Metaphorical, Futuristic, Philosophical) - Generates novel ideas and artistic interpretations.
* `π Data-Driven Analysis Squad`: (Analytical, Factual, Scientific) - Focuses on factual accuracy and logical deduction.
* `βοΈ Balanced Overview Group`: (Analytical, Philosophical, Metaphorical) - **This is the default team on startup.** Provides a well-rounded perspective.
* **Status:** Team selection is functional.
* **Individual Persona Toggles (e.g., Analytical, Scientific, etc.):**
* **Functionality:** If no team is selected (or "-- Select a Team (Optional) --" is chosen), you can manually toggle individual personas ON or OFF to form a custom team for the analysis.
* **Status:** Functional.
* **π Direct Mode:**
* **Functionality:** If ON, your query will be answered by a single, general-purpose LLM for a quick, direct response, bypassing the multi-persona/LangGraph system.
* **Status:** Functional.
* **β‘ Quick Mode:**
* **Functionality:** If ON, InsightFlow uses a smaller, predefined set of personas (currently Analytical and Factual) for a faster multi-perspective analysis. This overrides team/individual selections when active.
* **Status:** Functional.
* **ποΈ Show Individual Perspectives:**
* **Functionality:** If ON (default), after the main synthesized response, the individual contributions from each engaged persona will also be displayed.
* **Status:** Functional.
* **π¨ Show Visualizations:**
* **Functionality:** If ON (default), InsightFlow will attempt to generate and display a DALL-E image sketch and a Mermaid concept map related to the synthesized response.
* **Status:** Functional (DALL-E requires API key setup).
## Getting Started
1. **Review Settings (βοΈ):** Select a Persona Team or toggle individual personas. Ensure RAG is enabled if you want to try it with supported personas.
2. **Ask Your Question:** Type your query into the chat and press Enter.
3. **Observe:** Watch the progress updates as InsightFlow AI engages the personas, synthesizes insights, and generates visualizations.
We hope you find InsightFlow AI insightful!
"""
await cl.Message(content=help_text_md).send()
async def _initialize_embedding_model_in_session(force_openai: bool = False):
"""Helper to initialize/re-initialize embedding model based on settings."""
use_finetuned = cl.user_session.get("use_finetuned_embedding", False)
# Override if force_openai is true (e.g. for initial default)
if force_openai:
use_finetuned = False
cl.user_session.set("use_finetuned_embedding", False) # Ensure session reflects this override
current_model_details = cl.user_session.get("embedding_model_details", {})
new_model_id = ""
new_model_type = ""
if use_finetuned:
new_model_id = FINETUNED_BALANCED_TEAM_EMBED_ID
new_model_type = "hf"
else:
new_model_id = OPENAI_EMBED_MODEL_ID
new_model_type = "openai"
if current_model_details.get("id") == new_model_id and current_model_details.get("type") == new_model_type and cl.user_session.get("embedding_model_instance") is not None:
print(f"Embedding model '{new_model_id}' ({new_model_type}) already initialized and matches settings.")
return
print(f"Initializing embedding model: '{new_model_id}' ({new_model_type})...")
try:
embedding_instance = get_embedding_model_instance(new_model_id, new_model_type)
cl.user_session.set("embedding_model_instance", embedding_instance)
cl.user_session.set("embedding_model_details", {"id": new_model_id, "type": new_model_type})
print(f"Embedding model '{new_model_id}' ({new_model_type}) initialized and set in session.")
except Exception as e:
print(f"Error initializing embedding model '{new_model_id}': {e}")
cl.user_session.set("embedding_model_instance", None)
cl.user_session.set("embedding_model_details", {}) |