import os import textwrap import requests import gradio as gr import litellm from smolagents import CodeAgent, LiteLLMModel, tool, LogLevel MODEL_NAME = os.getenv("MODEL_NAME", "danielsheep/Qwen3-Coder-30B-A3B-Instruct-1M-Unsloth:UD-IQ3_XXS") OLLAMA_BASE = os.getenv("OLLAMA_URL", "http://127.0.0.1:11434") def make_model(): return LiteLLMModel( model_id=f"ollama_chat/{MODEL_NAME}", api_base=OLLAMA_BASE, num_ctx=16384, temperature=0.2, max_tokens=4096, ) @tool def web_search(query: str) -> str: """Ищет краткую информацию в интернете по текстовому запросу. Args: query: Текстовый поисковый запрос. """ resp = requests.get( "https://duckduckgo.com/html/", params={"q": query}, timeout=8, headers={"User-Agent": "qwen-agent-demo"}, ) resp.raise_for_status() text = resp.text return text[:4000] def run_multi_agent(spec: str, level: str, allow_internet: bool): model = make_model() # какие инструменты доступны агентам tools = [web_search] if allow_internet else [] # куски промптов, зависящие от allow_internet if allow_internet: web_hint_planner = """ В сгенерированном коде НЕ используй import requests и другие сетевые библиотеки. Для доступа в интернет у тебя есть ТОЛЬКО инструмент web_search(query: str). По фичам авторизации/безопасности ОБЯЗАТЕЛЬНО: - хотя бы один раз вызвать web_search с запросом вроде "mobile app login test cases", "best practices login screen testing"; - использовать найденную информацию, чтобы дополнить риски и сценарии. """ web_hint_tester = """ В сгенерированном коде НЕ используй import requests. Для доступа в интернет у тебя есть ТОЛЬКО инструмент web_search(query: str). Если фича связана с авторизацией/безопасностью: - вызови web_search минимум один раз, чтобы найти типичные позитивные, негативные и граничные кейсы; - добавь их в набор тестов. """ web_hint_reviewer = """ В сгенерированном коде НЕ используй import requests. Для доступа в интернет у тебя есть ТОЛЬКО инструмент web_search(query: str). Можешь использовать его, чтобы уточнить формулировки нефункциональных требований (безопасность, производительность, устойчивость) и включить их в отчёт. Не вставляй сырой HTML. """ else: web_hint_common = """ Интернет недоступен, инструмент web_search отсутствует. Не пытайся импортировать requests или вызывать web_search в коде. Опирайся только на своё знание модели. """ web_hint_planner = web_hint_common web_hint_tester = web_hint_common web_hint_reviewer = web_hint_common # Агент 1: планировщик planner = CodeAgent( tools=tools, model=model, add_base_tools=False, max_steps=5, ) plan_prompt = f""" Ты агент Planner. Всегда отвечай на РУССКОМ языке. {web_hint_planner} По описанию фичи составь план тестирования и список предположений. Структура результата (внутренняя, для следующих агентов): - Предположения (список) - Риски (список) - Декомпозиция сценариев (список коротких сценариев). Уровень детализации: {level}. Описание фичи: {spec} """ plan = planner.run(textwrap.dedent(plan_prompt)) # Агент 2: генератор тестов tester = CodeAgent( tools=tools, model=model, add_base_tools=False, max_steps=5, ) tests_prompt = f""" Ты агент Tester. Всегда отвечай на РУССКОМ языке. {web_hint_tester} На основе плана Planner сгенерируй НАБОР ТЕСТОВ как ПИТОНОВСКИЙ СПИСОК словарей. Каждый тест = словарь с полями: - "type" (positive / negative / boundary / nonfunctional), - "name", - "short_description". Не нужно писать текст отчёта, только структуру данных. Уровень детализации: {level}. План Planner (для анализа): {plan} """ tests = tester.run(textwrap.dedent(tests_prompt)) # Агент 3: ревьюер — формирует финальный Markdown reviewer = CodeAgent( tools=tools, model=model, add_base_tools=False, max_steps=3, # Достаточно шагов для исправления ошибок, но промпт явно указывает на финальный ответ ) reviewer_prompt = f""" Ты агент Reviewer. Всегда отвечай на РУССКОМ языке. {web_hint_reviewer} Твоя задача — СФОРМИРОВАТЬ ГОТОВЫЙ ОТЧЁТ ДЛЯ ЧЕЛОВЕКА в формате Markdown. КРИТИЧЕСКИ ВАЖНО: Это ФИНАЛЬНЫЙ шаг. Ты должен сгенерировать Python-код, который создаёт переменную report с Markdown-отчётом и возвращает её. После выполнения этого кода задача будет завершена. Формат кода должен быть: report = \"\"\" ## Предположения - ... ## Чеклист - ... ## Тесты ### Позитивные - Название: ... – краткое описание ### Негативные - ... ### Граничные - ... ### Нефункциональные - ... \"\"\" report Используй следующую структуру отчёта: ## Предположения - ... ## Чеклист - ... (краткий чеклист для ручного прогона) ## Тесты ### Позитивные - Название: ... – краткое описание ### Негативные - ... ### Граничные - ... ### Нефункциональные - ... Важные требования: - Сгенерируй Python-код с переменной report, содержащей Markdown-текст. - Используй тройные кавычки \"\"\" для многострочной строки. - В конце кода должна быть переменная report (без print, без return - просто report как последнее выражение). - Если код выполнился успешно и переменная report создана - это ФИНАЛЬНЫЙ ответ, больше ничего делать не нужно. - Если в коде была ошибка - исправь её и попробуй снова, но после успешного выполнения сразу остановись. - CodeAgent вернёт значение переменной report как финальный результат. - Все заголовки и описания на русском языке. - Используй информацию из Planner и Tester. - НЕ используй структуры dict/list в Markdown, только читабельный текст. - НЕ вызывай инструменты после создания report - это финальный шаг. - Используй final_answer() после получения достаточного результата План Planner: {plan} Структура тестов Tester: {tests} """ result = reviewer.run(textwrap.dedent(reviewer_prompt)) # CodeAgent возвращает результат последнего выражения в коде final_markdown = str(result).strip() if result is not None else "" return final_markdown with gr.Blocks(title="Multi-agent QA Assistant (Ollama + Qwen3)") as demo: gr.Markdown("# Multi-agent QA Assistant (Ollama + Qwen3)") gr.Markdown( f"- Model: `{MODEL_NAME}`\n" f"- Ollama: `{OLLAMA_BASE}`" ) spec = gr.Textbox( label="Описание фичи / сценария", lines=8, placeholder="Например: экран авторизации по номеру телефона с маской, автозаполнением, проверкой зарегистрированности..." ) level = gr.Dropdown( ["низкий", "средний", "высокий"], value="средний", label="Детализация" ) allow_internet = gr.Checkbox( label="Разрешить агенту выход в интернет (web_search)", value=False, ) run_btn = gr.Button("Запустить мультиагента") output = gr.Markdown() run_btn.click( fn=run_multi_agent, inputs=[spec, level, allow_internet], outputs=[output], ) def main(): demo.launch() if __name__ == "__main__": main()