JacekAI / agent /a11y_agent.py
Jacek Zadrożny
Add detailed logging and fix read-only filesystem issues
787b7ff
"""A11y Expert - Main accessibility question-answering agent."""
from typing import Optional, Generator
from openai import OpenAI
from langdetect import detect, LangDetectException
from config import get_settings
from agent.prompts import get_system_prompt
from agent.tools import search_knowledge_base
from database.vector_store_client import VectorStoreClient
from loguru import logger
class A11yExpertAgent:
"""Accessibility expert agent using OpenAI with RAG."""
def __init__(
self,
vector_store: VectorStoreClient,
llm_client: OpenAI,
language: str = "en",
expertise: str = "general"
):
"""
Initialize the A11y Expert agent.
Args:
vector_store: An instance of VectorStoreClient.
llm_client: An instance of OpenAI client.
language: 'pl' for Polish, 'en' for English.
expertise: 'general', 'wcag', or 'aria'.
"""
self.vector_store = vector_store
self.llm_client = llm_client
self.language = language
self.expertise = expertise
self.conversation_history = []
settings = get_settings()
self.model = settings.llm_model
self.system_prompt = get_system_prompt(language, expertise)
logger.info(f"A11yExpertAgent initialized (lang={language}, expertise={expertise})")
def close(self):
"""Close agent resources."""
try:
if self.vector_store:
self.vector_store.close()
if hasattr(self.llm_client, 'close'):
self.llm_client.close()
logger.info("A11yExpertAgent resources closed")
except Exception as e:
logger.warning(f"Error closing A11yExpertAgent: {e}")
def ask(self, question: str) -> Generator[str, None, None]:
"""
Ask a question and get a streaming answer with RAG.
Args:
question: Question about accessibility
Yields:
Answer chunks from the agent
"""
logger.info(f"Question: {question}")
try:
detected_lang = detect(question)
language = "pl" if detected_lang.startswith("pl") else "en"
except LangDetectException:
language = self.language
logger.info(f"Detected language: {language}")
# Dynamically update system prompt based on detected language
current_system_prompt = get_system_prompt(language, self.expertise)
logger.info("Searching knowledge base...")
context = search_knowledge_base(question, self.vector_store, language=language)
messages = [
{"role": "system", "content": current_system_prompt},
*self.conversation_history[-4:],
{"role": "user", "content": self._build_prompt_with_context(question, context, language)}
]
full_answer = ""
try:
response_stream = self.llm_client.chat.completions.create(
model=self.model,
messages=messages,
temperature=0.7,
max_tokens=1500,
top_p=0.9,
stream=True
)
for chunk in response_stream:
content = chunk.choices[0].delta.content
if content:
full_answer += content
yield content
self.conversation_history.append({"role": "user", "content": question})
self.conversation_history.append({"role": "assistant", "content": full_answer})
logger.info(f"Answer generated ({len(full_answer)} chars)")
except Exception as e:
logger.error(f"OpenAI API error: {e}")
yield f"Error during response generation: {e}"
def _build_prompt_with_context(self, question: str, context: str, language: str) -> str:
"""Build the prompt with context and language-specific instructions."""
if language == "pl":
return f"""
Na podstawie poniższego kontekstu z bazy wiedzy o dostępności, odpowiedz na pytanie.
=== KONTEKST Z BAZY WIEDZY ===
{context}
=== PYTANIE ===
{question}
=== ODPOWIEDŹ ===
KRYTYCZNE: Odpowiadaj WYŁĄCZNIE PO POLSKU. To pytanie jest po polsku, więc cała odpowiedź MUSI być po polsku.
Pamiętaj aby:
- Odpowiadać TYLKO po polsku (to jest najważniejsze!)
- Cytować konkretne kryteria i źródła
- Podawać praktyczne przykłady jeśli są istotne
- Być jasnym i zwięzłym
"""
else:
return f"""
Based on the following accessibility knowledge base context, answer the question.
=== KNOWLEDGE BASE CONTEXT ===
{context}
=== QUESTION ===
{question}
=== ANSWER ===
CRITICAL: Respond ONLY in ENGLISH. This question is in English, so your entire response MUST be in English.
Remember to:
- Answer ONLY in English (this is most important!)
- Cite specific criteria and sources
- Provide practical examples if relevant
- Be clear and concise
"""
def clear_history(self):
"""Clear conversation history."""
self.conversation_history = []
logger.info("Conversation history cleared")
def batch_ask(self, questions: list[str]) -> list[dict]:
"""Ask multiple questions in sequence."""
results = []
for question in questions:
try:
answer_chunks = [chunk for chunk in self.ask(question)]
answer = "".join(answer_chunks)
results.append({"question": question, "answer": answer, "success": True})
except Exception as e:
logger.error(f"Failed to answer '{question}': {e}")
results.append({"question": question, "answer": str(e), "success": False})
return results
def create_agent(language: Optional[str] = None) -> A11yExpertAgent:
"""Factory function to create and initialize agent."""
language = language or "en"
logger.info(f"Creating agent with language: {language}")
settings = get_settings()
# Create vector store with lazy connection (no DB access yet)
logger.info("Initializing vector store client...")
vector_store = VectorStoreClient(uri=settings.lancedb_uri)
api_key = settings.openai_api_key
logger.info("Initializing OpenAI client...")
client_args = {"api_key": api_key}
if settings.llm_base_url:
client_args["base_url"] = settings.llm_base_url
llm_client = OpenAI(**client_args)
logger.info("Creating A11yExpertAgent instance...")
agent = A11yExpertAgent(
vector_store=vector_store,
llm_client=llm_client,
language=language
)
logger.info("Agent creation complete")
return agent