File size: 8,789 Bytes
590d9bb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fd8a24a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
590d9bb
 
 
fd8a24a
590d9bb
fd8a24a
 
 
 
 
 
 
 
 
590d9bb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c5335db
590d9bb
 
 
c5335db
590d9bb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fd8a24a
590d9bb
fd8a24a
590d9bb
 
 
 
fd8a24a
dc37fde
 
fd8a24a
dc37fde
590d9bb
fd8a24a
c5335db
fd8a24a
590d9bb
fd8a24a
590d9bb
 
 
 
 
 
 
c5335db
590d9bb
 
 
 
 
 
c5335db
590d9bb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import gradio as gr
import os
import json
from dotenv import load_dotenv
from langchain_anthropic import ChatAnthropic

load_dotenv()

class LegionMariaAssistant:
    def __init__(self):
        # Router LLM - lightweight for section classification
        self.router_llm = ChatAnthropic(
            anthropic_api_key=os.getenv("ANTHROPIC_API_KEY"),
            model="claude-3-5-haiku-20241022",
            temperature=0.1
        )
        
        # Response LLM - for generating final answers
        self.response_llm = ChatAnthropic(
            anthropic_api_key=os.getenv("ANTHROPIC_API_KEY"),
            model="claude-3-5-haiku-20241022",
            temperature=0.1  # Lower temperature for more consistent, direct responses
        )
        
        self.data_content = {}
        self.load_data()
    
    def load_data(self):
        """Load structured JSON data"""
        data_file = "./data.json"
        if os.path.exists(data_file):
            try:
                with open(data_file, 'r', encoding='utf-8') as f:
                    self.data_content = json.load(f)
                
                print("Loaded Legion Maria JSON data successfully")
                print(f"Available sections: {list(self.data_content.keys())}")
                
            except Exception as e:
                print(f"Error loading data: {str(e)}")
                self.data_content = {}
        else:
            print("data.json not found")
            self.data_content = {}
    
    def route_query(self, message):
        """Router LLM decides which section of data to use"""
        available_sections = list(self.data_content.keys())
        
        router_prompt = f"""You are a query router for the Legion Maria Youth Affairs system. 
        
Available data sections: {available_sections}

DETAILED SECTION CONTENTS:

ABOUT section contains:
- organization, headquarters_location, head_office, contact info (phone, email, websites)
- mission, vision, core_values (faithfulness, inclusivity, service, formation, community, integrity)
- church_beliefs (core_belief, reach, membership, dress_code, sacred_practice)
- worship (practices, prohibited, prayer_schedule)

OFFICE section contains:
- departments (30+ departments with officers and roles like administration, treasury, education, procurement, events, medical, auditing, communications, projects, ICT, diaspora, facilities, music, logistics, grants, data engineering, coordination, security, etc.)
- completed_projects, pending_projects (washrooms, radio/TV station, hospital, mausoleum, refurbishments)
- current_activities (festivals, sports, workshops, conventions, camps)
- support_donations (mpesa, bank_account details)

LEADERSHIP section contains:
- supreme_leadership (patron_pope, matron_mother_superior, dean_of_cardinals)
- youth_affairs_director (title, contact)
- organizational_structure

User query: "{message}"

Respond with ONLY the most relevant section name. If the query spans multiple sections or is general, respond with "general".

ROUTING EXAMPLES:
- "Who is the director/pope/patron?" -> leadership
- "What is your mission/vision/values?" -> about
- "Where is headquarters/location/office?" -> about
- "Contact info/phone/email/website?" -> about
- "Tell me about projects/departments?" -> office
- "How to donate/support/contribute?" -> office
- "What activities do you do?" -> office
- "Church beliefs/worship/practices?" -> about
- "What do you do?" -> general

Section:"""

        try:
            response = self.router_llm.invoke([{"role": "user", "content": router_prompt}])
            section = response.content.strip().lower()
            
            # Validate section exists
            if section in available_sections:
                return section
            elif section == "general":
                return "general"
            else:
                return "about"  # Default fallback
                
        except Exception as e:
            print(f"Router error: {str(e)}")
            return "about"  # Default fallback
    
    def chat_response(self, message, history):
        """Two-tier LLM system: Router + Specialist response"""
        if not message.strip():
            return "Please ask me something about our Legion Maria Youth Affairs!"
        
        try:
            if not self.data_content:
                return "I don't have that information available right now."
            
            # Step 1: Router LLM decides which section to use
            selected_section = self.route_query(message)
            print(f"Router selected section: {selected_section}")
            
            # Step 2: Get relevant data based on routing decision
            if selected_section == "general":
                # Use all data for general queries
                relevant_data = self.data_content
            else:
                # Use only the specific section
                relevant_data = {selected_section: self.data_content.get(selected_section, {})}
            
            # Build conversation context
            conversation_context = ""
            if history:
                conversation_context = "Previous conversation:\n"
                for user_msg, assistant_msg in history[-3:]:  # Keep last 3 exchanges
                    conversation_context += f"User: {user_msg}\nAssistant: {assistant_msg}\n\n"
                conversation_context += "Current conversation:\n"
            
            # Step 3: Response LLM generates answer using only relevant data
            response_prompt = f"""You are Santa Legion from the Legion Maria Directorate of Youth Affairs. IMPORTANT: You must ONLY use the information provided below. Do not use any external knowledge or make assumptions.

ONLY USE THIS INFORMATION:
{json.dumps(relevant_data, indent=2)}

{conversation_context}User: {message}

STRICT RULES:
- You are Santa Legion, an assistant for the Legion Maria Youth Affairs (NOT the director yourself)
- Speak as "I" when referring to yourself, "we/our" for the organization
- ONLY answer using the information provided above
- When asked about leadership, refer to them in third person (e.g., "Our director is...")
- Keep responses SHORT (1-3 sentences maximum)
- Do not invent or assume any information not in the provided data
- Never mention being provided documents or data
- If asked about something not in your data, say "I don't have that information"

Answer based ONLY on the provided information:"""

            # Get response from specialist LLM
            response = self.response_llm.invoke([{"role": "user", "content": response_prompt}])
            return response.content

        except Exception as e:
            print(f"Error generating response: {str(e)}")
            return "I'm sorry, I'm having trouble right now. Please try again."

def main():
    assistant = LegionMariaAssistant()
    
    # Initial greeting message
    initial_greeting = [
        [None, "πŸ‘‹ Hello! I'm Santa Legion from the Legion Maria Youth Affairs. I'm here to help you learn about our mission, leadership, projects, and activities. What would you like to know?"]
    ]
    
    # Create mobile-optimized Gradio chat interface
    with gr.Blocks(title="Legion Maria Chat", theme=gr.themes.Soft()) as demo:
        gr.Markdown("# πŸ’¬ Legion Maria YA")
        
        # Mobile-optimized chat interface
        chatbot = gr.Chatbot(
            value=initial_greeting,
            height=400,  # Reduced for mobile
            show_label=False,
            container=True,
            bubble_full_width=True,  # Better for mobile
            show_share_button=False
        )
        
        # Mobile-friendly input layout
        with gr.Row():
            msg = gr.Textbox(
                placeholder="Ask me anything...",
                show_label=False,
                scale=5,
                container=False,
                lines=1
            )
            send_btn = gr.Button("πŸ“€", variant="primary", scale=1, size="sm")
        
        clear = gr.Button("πŸ”„ New Chat", variant="secondary", size="sm")
            
        # Chat functionality
        def respond(message, history):
            if message.strip():
                bot_response = assistant.chat_response(message, history)
                history.append([message, bot_response])
            return history, ""

        # Event handlers
        msg.submit(respond, [msg, chatbot], [chatbot, msg])
        send_btn.click(respond, [msg, chatbot], [chatbot, msg])
        clear.click(lambda: initial_greeting, None, chatbot)

    # Launch with better settings for chat app
    demo.launch(
        share=False,
        server_name="0.0.0.0",
        server_port=7860,
        show_api=False
    )

if __name__ == "__main__":
    main()