jaczad Claude Haiku 4.5 commited on
Commit
9bc459e
·
1 Parent(s): fa4b04d

Convert Q&A interface to conversational chatbot with memory

Browse files

app.py: replace static Q&A (Textbox in/out) with gr.ChatInterface +
gr.Chatbot(layout="bubble") embedded in gr.Blocks alongside notes panel.
ChatInterface handles history, Enter-to-submit, streaming, and clear
button automatically. Social sharing buttons retained below chat.

agent/a11y_agent.py: ask() now accepts optional history list[dict].
Passes last 6 history turns to LLM messages. Enriches RAG query with
last 2 user turns for context-aware retrieval on follow-up questions.

agent/prompts.py: rewrite PL and EN system prompts for conversational
style — agent now explicitly references prior conversation context,
adapts response format to question type (structured for requirements,
natural for follow-ups), and knows to say so when context is missing.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

Files changed (3) hide show
  1. agent/a11y_agent.py +26 -12
  2. agent/prompts.py +21 -33
  3. app.py +107 -254
agent/a11y_agent.py CHANGED
@@ -2,7 +2,7 @@
2
 
3
  import json
4
  from datetime import datetime
5
- from typing import Optional, Generator
6
  from openai import OpenAI
7
  from langdetect import detect, LangDetectException
8
  from config import get_settings
@@ -63,35 +63,49 @@ class A11yExpertAgent:
63
  except Exception as e:
64
  logger.warning(f"Error closing A11yExpertAgent: {e}")
65
 
66
- def ask(self, question: str) -> Generator[str, None, None]:
 
 
 
 
67
  """
68
  Ask a question and get a streaming answer with RAG.
69
-
70
  Args:
71
- question: Question about accessibility
72
-
 
73
  Yields:
74
- Answer chunks from the agent
75
  """
76
  logger.info(f"Question: {question}")
77
-
 
78
  try:
79
  detected_lang = detect(question)
80
  language = "pl" if detected_lang.startswith("pl") else "en"
81
  except LangDetectException:
82
  language = self.language
83
-
84
  logger.info(f"Detected language: {language}")
85
-
86
  current_system_prompt = self._prompts.get(language, self._prompts["en"])
87
 
 
 
 
 
 
 
88
  logger.info("Searching knowledge base...")
89
- context, sources = search_knowledge_base(question, self.vector_store, language=language)
90
 
 
 
91
  messages = [
92
  {"role": "system", "content": current_system_prompt},
93
- # Stateless: no conversation history in context
94
- {"role": "user", "content": self._build_prompt_with_context(question, context, language)}
95
  ]
96
 
97
  full_answer = ""
 
2
 
3
  import json
4
  from datetime import datetime
5
+ from typing import Optional, Generator, List, Dict
6
  from openai import OpenAI
7
  from langdetect import detect, LangDetectException
8
  from config import get_settings
 
63
  except Exception as e:
64
  logger.warning(f"Error closing A11yExpertAgent: {e}")
65
 
66
+ def ask(
67
+ self,
68
+ question: str,
69
+ history: Optional[List[Dict[str, str]]] = None,
70
+ ) -> Generator[str, None, None]:
71
  """
72
  Ask a question and get a streaming answer with RAG.
73
+
74
  Args:
75
+ question: Current user message.
76
+ history: Conversation history as list of {"role": ..., "content": ...} dicts.
77
+
78
  Yields:
79
+ Answer chunks from the agent.
80
  """
81
  logger.info(f"Question: {question}")
82
+ history = history or []
83
+
84
  try:
85
  detected_lang = detect(question)
86
  language = "pl" if detected_lang.startswith("pl") else "en"
87
  except LangDetectException:
88
  language = self.language
89
+
90
  logger.info(f"Detected language: {language}")
91
+
92
  current_system_prompt = self._prompts.get(language, self._prompts["en"])
93
 
94
+ # Enrich RAG query with recent user turns for context-aware retrieval
95
+ recent_user_turns = [
96
+ m["content"] for m in history[-4:] if m.get("role") == "user"
97
+ ]
98
+ rag_query = " ".join(recent_user_turns[-2:] + [question]) if recent_user_turns else question
99
+
100
  logger.info("Searching knowledge base...")
101
+ context, sources = search_knowledge_base(rag_query, self.vector_store, language=language)
102
 
103
+ # Build messages: system + last 6 history turns + current user turn with context
104
+ history_messages = history[-6:]
105
  messages = [
106
  {"role": "system", "content": current_system_prompt},
107
+ *history_messages,
108
+ {"role": "user", "content": self._build_prompt_with_context(question, context, language)},
109
  ]
110
 
111
  full_answer = ""
agent/prompts.py CHANGED
@@ -1,24 +1,16 @@
1
  """System prompts for A11y Expert agent in different languages."""
2
 
3
- SYSTEM_PROMPT_PL = """Jesteś ekspertem dostępności cyfrowej specjalizującym się w WCAG 2.2, WAI-ARIA i prawodawstwie EU.
 
4
 
5
- KRYTYCZNE: Odpowiadaj ZAWSZE I WYŁĄCZNIE PO POLSKU. Nawet jeśli otrzymujesz angielskie źródła, tłumacz je i odpowiadaj po polsku.
6
 
7
- FORMAT ODPOWIEDZI (stosuj gdy dotyczy konkretnych wymagań):
8
- 1. **Kryterium sukcesu**: numer i nazwa (np. 1.4.3 Kontrast (Minimalny))
9
- 2. **Poziom zgodności**: A / AA / AAA
10
- 3. **Wymaganie**: co dokładnie trzeba spełnić
11
- 4. **Jak to osiągnąć**: praktyczne techniki i przykłady
12
- 5. **Źródła**: które fragmenty z bazy wiedzy wykorzystałeś
13
-
14
- ZASADY:
15
- - Odpowiadaj wyłącznie po polsku
16
- - Cytuj konkretne kryteria sukcesu z numerem (np. SC 1.1.1)
17
- - Jeśli kontekst z bazy wiedzy nie zawiera odpowiedzi — powiedz to wprost, nie zgaduj
18
- - Używaj prostego, zrozumiałego języka
19
- - Dawaj praktyczne przykłady kodu gdy pytanie dotyczy implementacji
20
-
21
- Odpowiadasz bezpośrednio na pytanie w oparciu o dostarczony kontekst."""
22
 
23
  SYSTEM_PROMPT_EN = """
24
  You are an accessibility expert specializing in:
@@ -29,26 +21,22 @@ You are an accessibility expert specializing in:
29
 
30
  ⚠️ CRITICAL: You respond ONLY in ENGLISH. All your responses MUST be in English, even if sources are in other languages.
31
 
32
- RESPONSE FORMAT (use when answering specific requirements):
33
- 1. **Success Criterion**: number and name (e.g., 1.4.3 Contrast (Minimum))
34
- 2. **Conformance Level**: A / AA / AAA
35
- 3. **Requirement**: what exactly must be met
36
- 4. **How to achieve it**: practical techniques and code examples
37
- 5. **Sources**: which knowledge base fragments you used
38
 
39
- MANDATORY RULES:
40
- 1. ALWAYS respond in ENGLISH - translate non-English sources, never respond in other languages
41
- 2. ALWAYS cite specific WCAG success criteria with number (e.g., SC 1.4.3)
42
- 3. If the knowledge base context doesn't contain the answer — say so explicitly, do not guess
43
- 4. Use simple, understandable language - avoid jargon unless necessary
44
- 5. Provide practical code examples or implementation guidance when relevant
45
- 6. ✅ Reference both WCAG requirements and industry best practices
46
 
47
  HOW YOU WORK:
48
  - You receive context from the WCAG/ARIA knowledge base (relevance rated ★★★/★★☆/★☆☆)
49
- - Prefer ★★★ sources; use ★☆☆ sources only when nothing better is available
50
- - You respond DIRECTLY to the question (don't talk about searching the database!)
51
- - Focus on the content, not the process
52
  """
53
 
54
  SYSTEM_PROMPT_WCAG_EXPERT = """
 
1
  """System prompts for A11y Expert agent in different languages."""
2
 
3
+ SYSTEM_PROMPT_PL = """Jesteś Jacek AI konwersacyjny ekspert dostępności cyfrowej (WCAG 2.2, WAI-ARIA, prawo EU).
4
+ Prowadzisz rozmowę: pamiętasz co zostało powiedziane wcześniej i nawiązujesz do poprzednich odpowiedzi.
5
 
6
+ KRYTYCZNE: Odpowiadaj ZAWSZE I WYŁĄCZNIE PO POLSKU. Nawet jeśli otrzymujesz angielskie źródła, tłumacz je.
7
 
8
+ STYL ODPOWIEDZI:
9
+ - Na pytania o konkretne wymagania: podaj numer kryterium (np. SC 1.4.3), poziom (A/AA/AAA), co trzeba spełnić, jak to osiągnąć
10
+ - Na pytania ogólne lub follow-up: odpowiadaj naturalnie, nawiązując do kontekstu rozmowy
11
+ - Cytuj konkretne kryteria sukcesu z numerem gdy to istotne
12
+ - Jeśli baza wiedzy nie zawiera odpowiedzi — powiedz wprost, nie zgaduj
13
+ - Dawaj przykłady kodu gdy pytanie dotyczy implementacji"""
 
 
 
 
 
 
 
 
 
14
 
15
  SYSTEM_PROMPT_EN = """
16
  You are an accessibility expert specializing in:
 
21
 
22
  ⚠️ CRITICAL: You respond ONLY in ENGLISH. All your responses MUST be in English, even if sources are in other languages.
23
 
24
+ You are Jacek AI — a conversational accessibility expert (WCAG 2.2, WAI-ARIA, EU law).
25
+ You maintain conversation context: remember what was discussed and build on previous answers.
26
+
27
+ ⚠️ CRITICAL: You respond ONLY in ENGLISH. Translate any non-English sources.
 
 
28
 
29
+ RESPONSE STYLE:
30
+ - For specific requirements: cite the criterion number (e.g., SC 1.4.3), conformance level (A/AA/AAA), what must be met, how to achieve it
31
+ - For general or follow-up questions: respond naturally, referencing prior conversation context
32
+ - Cite specific WCAG success criteria with numbers when relevant
33
+ - If the knowledge base lacks the answer say so explicitly, do not guess
34
+ - Provide code examples when the question concerns implementation
 
35
 
36
  HOW YOU WORK:
37
  - You receive context from the WCAG/ARIA knowledge base (relevance rated ★★★/★★☆/★☆☆)
38
+ - Prefer ★★★ sources; treat ★☆☆ sources as supplementary only
39
+ - Respond DIRECTLY don't mention searching the database
 
40
  """
41
 
42
  SYSTEM_PROMPT_WCAG_EXPERT = """
app.py CHANGED
@@ -1,340 +1,193 @@
1
  """
2
- Gradio UI for Jacek AI with lazy initialization.
3
- This module creates a Gradio interface that starts FAST,
4
- then initializes the agent in the background.
5
  """
6
  import sys
7
  import os
 
 
 
8
 
9
- # Suppress asyncio cleanup warnings by setting environment variable
10
  os.environ['PYTHONUNBUFFERED'] = '1'
11
-
12
- # Suppress all asyncio warnings at the earliest possible point
13
- import warnings
14
  warnings.filterwarnings('ignore', category=ResourceWarning)
15
 
16
  import gradio as gr
17
  from loguru import logger
18
- import atexit
19
- import threading
20
  from agent.a11y_agent import create_agent, A11yExpertAgent
21
  from config import get_settings
22
 
23
  # --- Setup ---
24
- # Configure logger
25
  logger.remove()
26
  logger.add(sys.stderr, level=get_settings().log_level)
27
 
28
- # Global agent instance
29
  agent_instance: A11yExpertAgent = None
30
  agent_ready = False
31
  agent_error = None
32
 
 
33
  # --- Agent Initialization ---
34
  def initialize_agent_background():
35
- """Initialize the agent in background thread."""
36
  global agent_instance, agent_ready, agent_error
37
-
38
  try:
39
  logger.info("🔄 Starting agent initialization in background...")
40
- import time
41
-
42
- logger.info("⏱️ Sleeping 2 seconds to avoid race condition...")
43
  time.sleep(2)
44
-
45
- logger.info("📦 Calling create_agent()...")
46
  agent_instance = create_agent()
47
-
48
- logger.info("✓ Agent instance created, setting ready flag...")
49
  agent_ready = True
50
  logger.success("✅ A11y Expert Agent is ready!")
51
  except Exception as e:
52
- logger.error(f"❌ Failed to initialize agent: {e}")
53
  import traceback
54
- logger.error(traceback.format_exc())
55
  agent_error = str(e)
56
  agent_instance = None
57
 
58
- def cleanup_resources():
59
- """Clean up resources on app shutdown."""
60
- global agent_instance
61
- logger.info("Cleaning up resources...")
62
- try:
63
- # Close agent and all its resources
64
- if agent_instance:
65
- agent_instance.close()
66
-
67
- # Close embeddings client singleton if it exists
68
- from models.embeddings import get_embeddings_client
69
- if hasattr(get_embeddings_client, '_instance'):
70
- get_embeddings_client._instance.close()
71
-
72
- logger.success("✅ Resources cleaned up successfully")
73
- except Exception as e:
74
- logger.warning(f"Error during cleanup: {e}")
75
 
76
- # --- Gradio Chat Logic ---
77
- def respond(message: str):
78
- """
79
- Main function for the Gradio Q&A interface.
80
- Receives a user question and uses the agent to generate a streaming response.
81
-
82
- Args:
83
- message: The user's question.
84
-
85
- Yields:
86
- A stream of response chunks to update the UI.
87
- """
88
  global agent_instance, agent_ready, agent_error
89
-
90
- # Wait for background initialization if not ready yet
91
- if not agent_ready and not agent_error and agent_instance is None:
92
- yield "⏳ Agent is initializing in background, please wait a moment..."
93
- import time
94
- # Wait up to 15 seconds for background initialization
95
- for i in range(30):
96
- time.sleep(0.5)
97
- if agent_ready:
98
- break
99
-
100
- # If still not ready, initialize synchronously as fallback
101
- if not agent_ready and agent_instance is None:
102
- yield "⏳ Finalizing agent initialization..."
103
- try:
104
- logger.info("🔄 Background init not complete, initializing synchronously...")
105
- agent_instance = create_agent()
106
- agent_ready = True
107
- logger.success("✅ Jacek AI Agent is ready!")
108
- except Exception as e:
109
- logger.error(f"❌ Failed to initialize agent: {e}")
110
- import traceback
111
- logger.error(traceback.format_exc())
112
- agent_error = str(e)
113
- agent_instance = None
114
- yield f"❌ Agent initialization failed: {agent_error}"
115
- return
116
-
117
- # Check if agent failed to initialize
118
  if agent_error:
119
- yield f"❌ Agent initialization failed: {agent_error}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  return
121
-
122
- if not agent_instance:
123
- yield "❌ Agent not available. Please check logs for errors."
 
124
  return
125
 
126
- logger.info(f"User query: '{message}'")
127
  full_response = ""
128
  try:
129
- for chunk in agent_instance.ask(message):
130
  full_response += chunk
131
  yield full_response
132
  except Exception as e:
133
- logger.error(f"Error during response generation: {e}")
134
- yield f"An error occurred: {e}"
135
-
136
 
137
- # --- Gradio UI Definition ---
138
- # Q&A Form layout: Question form on left, Notes on right
139
- # Get public URL from config for social sharing
140
- SHARE_URL = get_settings().public_url
141
 
142
- # Custom HTML head for Open Graph meta tags (for LinkedIn/Facebook previews)
143
- custom_head = """
144
- <meta property="og:title" content="Jacek AI - Ekspert Dostępności Cyfrowej" />
145
- <meta property="og:description" content="Uzyskaj odpowiedzi na pytania o WCAG, ARIA i najlepsze praktyki dostępności cyfrowej. Chatbot oparty na sztucznej inteligencji." />
146
- <meta property="og:type" content="website" />
147
- <meta property="og:url" content="https://huggingface.co/spaces/jaczad/JacekAI" />
148
- <meta name="twitter:card" content="summary_large_image" />
149
- <meta name="twitter:title" content="Jacek AI - Ekspert Dostępności" />
150
- <meta name="twitter:description" content="Chatbot AI odpowiadający na pytania o dostępność cyfrową (WCAG, ARIA)" />
151
- """
 
 
 
 
152
 
153
- with gr.Blocks(title="Jacek AI", head=custom_head) as demo:
154
- gr.Markdown("# 🎯 Jacek AI")
155
- gr.Markdown("**Ekspert dostępności cyfrowej** - Uzyskaj odpowiedzi na pytania o WCAG, ARIA i najlepsze praktyki.")
156
- gr.Markdown("ℹ️ *Pytania i odpowiedzi są zapisywane anonimowo w celu poprawy systemu i budowy zestawu danych treningowych.*")
157
 
158
- with gr.Row():
159
- # Left column: Q&A Form
160
- with gr.Column(scale=1):
161
- gr.Markdown("### 📋 Zadaj pytanie ekspertowi")
162
 
163
- question_input = gr.Textbox(
164
- label="Twoje pytanie lub problem:",
165
- placeholder="Opisz problem z dostępnością, zapytaj o wymagania WCAG, poproś o przegląd kodu... (Enter wysyła, Shift+Enter = nowa linia)",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
  lines=1,
167
- max_lines=8,
168
- max_length=1000,
169
- show_label=True
 
170
  )
171
 
172
- submit_btn = gr.Button("🔍 Zapytaj eksperta", variant="primary", size="lg")
173
-
174
- gr.Markdown("---")
175
- gr.Markdown("### 📊 Odpowiedź eksperta:")
176
-
177
- answer_output = gr.Textbox(
178
- value="*Odpowiedź pojawi się tutaj po zadaniu pytania...*",
179
- show_label=False,
180
- elem_id="answer_output",
181
- lines=15,
182
- max_lines=30,
183
- interactive=False,
184
- container=False
185
  )
186
 
187
- # Action buttons
188
  with gr.Row():
189
- copy_btn = gr.Button("📋 Kopiuj odpowiedź", variant="secondary", size="sm")
190
  share_twitter = gr.Button("🐦 X/Twitter", variant="secondary", size="sm")
191
  share_linkedin = gr.Button("💼 LinkedIn", variant="secondary", size="sm")
192
  share_facebook = gr.Button("📘 Facebook", variant="secondary", size="sm")
193
 
194
- gr.Markdown("---")
195
-
196
- # Right column: Markdown content from file
197
  with gr.Column(scale=1):
198
  gr.Markdown("### 📝 Notatki")
199
-
200
- def load_notes():
201
- """Load notes from notes.md file."""
202
- try:
203
- with open("notes.md", "r", encoding="utf-8") as f:
204
- return f.read()
205
- except FileNotFoundError:
206
- return """
207
- ## Witaj w Jacek AI! 👋
208
-
209
- Stwórz plik `notes.md` w katalogu projektu aby zobaczyć tutaj swoje notatki.
210
-
211
- ### Przydatne linki:
212
- - [WCAG 2.2 Guidelines](https://www.w3.org/WAI/WCAG22/quickref/)
213
- - [ARIA Authoring Practices](https://www.w3.org/WAI/ARIA/apg/)
214
- - [MDN Accessibility](https://developer.mozilla.org/en-US/docs/Web/Accessibility)
215
- """
216
- except Exception as e:
217
- return f"⚠️ Błąd wczytywania notes.md: {e}"
218
-
219
- markdown_content = gr.Markdown(
220
- value=load_notes(),
221
- show_label=False,
222
- elem_id="notes_display"
223
- )
224
-
225
- refresh_btn = gr.Button("🔄 Odśwież notatki", variant="secondary")
226
- refresh_btn.click(
227
- fn=load_notes,
228
- outputs=markdown_content
229
- )
230
-
231
- # Q&A logic
232
- def handle_question(question):
233
- """Process question and return answer."""
234
- if not question or not question.strip():
235
- return "⚠️ Proszę wpisać pytanie."
236
 
237
- # Show loading message
238
- yield "⏳ Analizuję pytanie i przeszukuję bazę wiedzy..."
239
-
240
- # Generate answer (streaming)
241
- for chunk in respond(question):
242
- yield chunk
243
-
244
- # Wire up the Q&A form
245
- submit_btn.click(
246
- fn=handle_question,
247
- inputs=question_input,
248
- outputs=answer_output
249
- )
250
-
251
- question_input.submit(
252
- fn=handle_question,
253
- inputs=question_input,
254
- outputs=answer_output
255
- )
256
-
257
- # Copy button - copies the answer text (no alert)
258
- copy_btn.click(
259
- fn=None,
260
- inputs=answer_output,
261
- outputs=None,
262
- js="""
263
- (answer) => {
264
- navigator.clipboard.writeText(answer);
265
- }
266
- """
267
- )
268
-
269
- # Social media share buttons
270
  share_twitter.click(
271
  fn=None,
272
- inputs=[question_input, answer_output],
273
- outputs=None,
274
  js=f"""
275
- (question, answer) => {{
276
- // Prepare tweet with full question and answer (preserve paragraph breaks)
277
- const cleanAnswer = answer
278
- .replace(/#+\\s*/g, '') // Remove markdown headers
279
- .replace(/\\n{{3,}}/g, '\\n\\n') // Multiple newlines -> double newline
280
- .replace(/\\*\\*/g, '') // Remove bold markdown
281
- .trim();
282
-
283
- const text = `Zapytałem Jacek AI: "${{question}}"\\n\\n${{cleanAnswer}}\\n\\n🔗 `;
284
- const url = '{SHARE_URL}';
285
-
286
- // Twitter automatically appends URL, but we add it in text too for clarity
287
- window.open(`https://twitter.com/intent/tweet?text=${{encodeURIComponent(text + url)}}`, '_blank');
288
  }}
289
  """
290
  )
291
-
292
  share_linkedin.click(
293
  fn=None,
294
- inputs=[question_input, answer_output],
295
- outputs=None,
296
- js=f"""
297
- (question, answer) => {{
298
- const url = '{SHARE_URL}';
299
- // LinkedIn API limitation: cannot pre-fill post text (security policy)
300
- // Only URL is shared - LinkedIn scrapes Open Graph meta tags for preview
301
- window.open(`https://www.linkedin.com/sharing/share-offsite/?url=${{encodeURIComponent(url)}}`, '_blank');
302
- }}
303
- """
304
  )
305
-
306
  share_facebook.click(
307
  fn=None,
308
- inputs=[question_input, answer_output],
309
- outputs=None,
310
- js=f"""
311
- (question, answer) => {{
312
- const url = '{SHARE_URL}';
313
- // Facebook API limitation: cannot pre-fill post text (security policy)
314
- // Only URL is shared - Facebook scrapes Open Graph meta tags for preview
315
- window.open(`https://www.facebook.com/sharer/sharer.php?u=${{encodeURIComponent(url)}}`, '_blank');
316
- }}
317
- """
318
  )
 
319
 
320
 
321
- # --- App Launch ---
322
- # Register cleanup handler
323
- # atexit.register(cleanup_resources) # Disabled: Causes premature shutdown on Hugging Face Spaces
324
-
325
- # Start background initialization
326
- logger.info("🚀 Starting Gradio app with background agent initialization...")
327
  init_thread = threading.Thread(target=initialize_agent_background, daemon=True)
328
  init_thread.start()
329
- logger.info("ℹ️ Agent initialization started in background")
330
 
331
- # For Hugging Face Spaces, we need to either:
332
- # 1. Have a variable named 'demo' (which we have)
333
- # 2. Or explicitly call demo.queue() to enable the app
334
- # We'll use queue() to ensure proper startup
335
  if __name__ == "__main__":
336
  demo.queue()
337
  demo.launch()
338
  else:
339
- # On HF Spaces, just ensure demo is ready
340
  demo.queue()
 
1
  """
2
+ Gradio conversational UI for Jacek AI with lazy initialization.
3
+ Uses gr.ChatInterface for conversation with history and streaming.
 
4
  """
5
  import sys
6
  import os
7
+ import time
8
+ import threading
9
+ import warnings
10
 
 
11
  os.environ['PYTHONUNBUFFERED'] = '1'
 
 
 
12
  warnings.filterwarnings('ignore', category=ResourceWarning)
13
 
14
  import gradio as gr
15
  from loguru import logger
 
 
16
  from agent.a11y_agent import create_agent, A11yExpertAgent
17
  from config import get_settings
18
 
19
  # --- Setup ---
 
20
  logger.remove()
21
  logger.add(sys.stderr, level=get_settings().log_level)
22
 
 
23
  agent_instance: A11yExpertAgent = None
24
  agent_ready = False
25
  agent_error = None
26
 
27
+
28
  # --- Agent Initialization ---
29
  def initialize_agent_background():
 
30
  global agent_instance, agent_ready, agent_error
 
31
  try:
32
  logger.info("🔄 Starting agent initialization in background...")
 
 
 
33
  time.sleep(2)
 
 
34
  agent_instance = create_agent()
 
 
35
  agent_ready = True
36
  logger.success("✅ A11y Expert Agent is ready!")
37
  except Exception as e:
 
38
  import traceback
39
+ logger.error(f"❌ Failed to initialize agent: {e}\n{traceback.format_exc()}")
40
  agent_error = str(e)
41
  agent_instance = None
42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
 
44
+ def _ensure_agent():
 
 
 
 
 
 
 
 
 
 
 
45
  global agent_instance, agent_ready, agent_error
46
+ if agent_ready:
47
+ return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  if agent_error:
49
+ return agent_error
50
+ for _ in range(30):
51
+ time.sleep(0.5)
52
+ if agent_ready:
53
+ return None
54
+ if agent_error:
55
+ return agent_error
56
+ if not agent_ready:
57
+ try:
58
+ agent_instance = create_agent()
59
+ agent_ready = True
60
+ except Exception as e:
61
+ agent_error = str(e)
62
+ return str(e)
63
+ return None
64
+
65
+
66
+ # --- Chat function ---
67
+ def chat(message: str, history: list):
68
+ """
69
+ Streaming chat function for gr.ChatInterface.
70
+ history: list of {"role": "user"/"assistant", "content": str}
71
+ Yields partial assistant response strings.
72
+ """
73
+ if not message or not message.strip():
74
+ yield ""
75
  return
76
+
77
+ err = _ensure_agent()
78
+ if err:
79
+ yield f"❌ Błąd inicjalizacji agenta: {err}"
80
  return
81
 
82
+ logger.info(f"User: {message!r}")
83
  full_response = ""
84
  try:
85
+ for chunk in agent_instance.ask(message, history=history):
86
  full_response += chunk
87
  yield full_response
88
  except Exception as e:
89
+ logger.error(f"Error during response: {e}")
90
+ yield f" Błąd: {e}"
 
91
 
 
 
 
 
92
 
93
+ def load_notes():
94
+ try:
95
+ with open("notes.md", "r", encoding="utf-8") as f:
96
+ return f.read()
97
+ except FileNotFoundError:
98
+ return (
99
+ "## Witaj w Jacek AI! 👋\n\n"
100
+ "Stwórz `notes.md` aby wyświetlić notatki.\n\n"
101
+ "- [WCAG 2.2](https://www.w3.org/WAI/WCAG22/quickref/)\n"
102
+ "- [WAI-ARIA](https://www.w3.org/WAI/ARIA/apg/)\n"
103
+ "- [MDN Accessibility](https://developer.mozilla.org/en-US/docs/Web/Accessibility)"
104
+ )
105
+ except Exception as e:
106
+ return f"⚠️ Błąd: {e}"
107
 
 
 
 
 
108
 
109
+ # --- UI ---
110
+ SHARE_URL = get_settings().public_url
 
 
111
 
112
+ with gr.Blocks(title="Jacek AI") as demo:
113
+ gr.Markdown("# 🎯 Jacek AI")
114
+ gr.Markdown(
115
+ "**Konwersacyjny ekspert dostępności cyfrowej** — zadawaj pytania o WCAG, ARIA "
116
+ "i najlepsze praktyki. Asystent pamięta kontekst rozmowy."
117
+ )
118
+ gr.Markdown("ℹ️ *Rozmowy są zapisywane anonimowo w celu poprawy systemu.*")
119
+
120
+ with gr.Row(equal_height=False):
121
+ # Left: chat
122
+ with gr.Column(scale=3):
123
+ chatbot_widget = gr.Chatbot(
124
+ value=[],
125
+ label="Jacek AI",
126
+ show_label=False,
127
+ layout="bubble",
128
+ height=520,
129
+ render_markdown=True,
130
+ placeholder=(
131
+ "<br><br><center><b>🎯 Jacek AI</b><br>"
132
+ "Ekspert dostępności cyfrowej<br><br>"
133
+ "Zadaj pytanie o WCAG, ARIA lub dostępność</center>"
134
+ ),
135
+ )
136
+ textbox_widget = gr.Textbox(
137
+ placeholder="Wpisz pytanie… (Enter wysyła, Shift+Enter = nowa linia)",
138
  lines=1,
139
+ max_lines=5,
140
+ max_length=2000,
141
+ show_label=False,
142
+ container=False,
143
  )
144
 
145
+ chat_interface = gr.ChatInterface(
146
+ fn=chat,
147
+ chatbot=chatbot_widget,
148
+ textbox=textbox_widget,
149
+ autofocus=True,
 
 
 
 
 
 
 
 
150
  )
151
 
 
152
  with gr.Row():
 
153
  share_twitter = gr.Button("🐦 X/Twitter", variant="secondary", size="sm")
154
  share_linkedin = gr.Button("💼 LinkedIn", variant="secondary", size="sm")
155
  share_facebook = gr.Button("📘 Facebook", variant="secondary", size="sm")
156
 
157
+ # Right: notes
 
 
158
  with gr.Column(scale=1):
159
  gr.Markdown("### 📝 Notatki")
160
+ notes_display = gr.Markdown(value=load_notes(), show_label=False)
161
+ refresh_btn = gr.Button("🔄 Odśwież notatki", variant="secondary", size="sm")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
 
163
+ # Social sharing
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
  share_twitter.click(
165
  fn=None,
 
 
166
  js=f"""
167
+ () => {{
168
+ const text = 'Rozmawiam z Jacek AI ekspertem dostępności cyfrowej (WCAG, ARIA) 🔗 {SHARE_URL}';
169
+ window.open('https://twitter.com/intent/tweet?text=' + encodeURIComponent(text), '_blank');
 
 
 
 
 
 
 
 
 
 
170
  }}
171
  """
172
  )
 
173
  share_linkedin.click(
174
  fn=None,
175
+ js=f"() => window.open('https://www.linkedin.com/sharing/share-offsite/?url=' + encodeURIComponent('{SHARE_URL}'), '_blank')"
 
 
 
 
 
 
 
 
 
176
  )
 
177
  share_facebook.click(
178
  fn=None,
179
+ js=f"() => window.open('https://www.facebook.com/sharer/sharer.php?u=' + encodeURIComponent('{SHARE_URL}'), '_blank')"
 
 
 
 
 
 
 
 
 
180
  )
181
+ refresh_btn.click(fn=load_notes, outputs=notes_display)
182
 
183
 
184
+ # --- Launch ---
185
+ logger.info("🚀 Starting Jacek AI (conversational mode)...")
 
 
 
 
186
  init_thread = threading.Thread(target=initialize_agent_background, daemon=True)
187
  init_thread.start()
 
188
 
 
 
 
 
189
  if __name__ == "__main__":
190
  demo.queue()
191
  demo.launch()
192
  else:
 
193
  demo.queue()