Вынес агентов в отдельные файлы
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
__pycache__
|
__pycache__
|
||||||
|
report_*.json
|
||||||
4
agent/__init__.py
Normal file
4
agent/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from .developer import DeveloperAgent
|
||||||
|
from .reviewer import ReviewerAgent
|
||||||
|
|
||||||
|
__all__ = ["DeveloperAgent", "ReviewerAgent"]
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
# agents.py
|
|
||||||
from typing import Optional, Dict, Any
|
from typing import Optional, Dict, Any
|
||||||
from langchain_openai import ChatOpenAI
|
|
||||||
from langchain_core.messages import HumanMessage, SystemMessage
|
from langchain_core.messages import HumanMessage, SystemMessage
|
||||||
from langchain_community.chat_models import ChatOllama
|
from langchain_community.chat_models import ChatOllama
|
||||||
import json
|
import json
|
||||||
@@ -15,7 +13,8 @@ class DeveloperAgent:
|
|||||||
self.llm = ChatOllama(
|
self.llm = ChatOllama(
|
||||||
model=MODEL_NAME,
|
model=MODEL_NAME,
|
||||||
temperature=TEMPERATURE,
|
temperature=TEMPERATURE,
|
||||||
base_url=LLM_HOST
|
base_url=LLM_HOST,
|
||||||
|
num_predict=8192
|
||||||
)
|
)
|
||||||
|
|
||||||
def develop(self, task: Task, previous_comments: list = None) -> CodeSolution:
|
def develop(self, task: Task, previous_comments: list = None) -> CodeSolution:
|
||||||
@@ -136,86 +135,3 @@ class DeveloperAgent:
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
return None
|
return None
|
||||||
class ReviewerAgent:
|
|
||||||
"""Агент-ревьювер: проверяет качество кода"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.llm = ChatOllama(
|
|
||||||
model=MODEL_NAME,
|
|
||||||
temperature=0.1,
|
|
||||||
base_url=LLM_HOST
|
|
||||||
)
|
|
||||||
|
|
||||||
def review(self, task: Task, solution: CodeSolution) -> ReviewResult:
|
|
||||||
"""Проверяет код и возвращает замечания"""
|
|
||||||
|
|
||||||
system_prompt = """Ты - строгий ревьювер кода. Твоя задача:
|
|
||||||
1. Проверить соответствие требованиям
|
|
||||||
2. Найти логические ошибки и баги
|
|
||||||
3. Оценить читаемость и стиль кода
|
|
||||||
4. Проверить покрытие тестами
|
|
||||||
5. Найти проблемы безопасности
|
|
||||||
6. Предложить улучшения
|
|
||||||
|
|
||||||
Оценивай код критически. Будь конкретен в замечаниях, указывай файлы и строки.
|
|
||||||
|
|
||||||
Верни JSON с полями:
|
|
||||||
- status: "approved", "changes_requested", или "rejected"
|
|
||||||
- comments: массив объектов с полями: file, line, severity, text
|
|
||||||
- summary: краткое резюме ревью
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Формируем представление кода для ревью
|
|
||||||
code_for_review = ""
|
|
||||||
for filename, content in solution.files.items():
|
|
||||||
code_for_review += f"\n--- {filename} ---\n{content}\n"
|
|
||||||
|
|
||||||
if solution.tests:
|
|
||||||
code_for_review += "\n--- TESTS ---\n"
|
|
||||||
for testname, test_content in solution.tests.items():
|
|
||||||
code_for_review += f"\n--- {testname} ---\n{test_content}\n"
|
|
||||||
|
|
||||||
user_prompt = f"""
|
|
||||||
Требование: {task.requirement}
|
|
||||||
|
|
||||||
Код на ревью:
|
|
||||||
{code_for_review}
|
|
||||||
|
|
||||||
Описание решения разработчика: {solution.description}
|
|
||||||
|
|
||||||
Проведи ревью. Будь строг, но справедлив.
|
|
||||||
"""
|
|
||||||
|
|
||||||
messages = [
|
|
||||||
SystemMessage(content=system_prompt),
|
|
||||||
HumanMessage(content=user_prompt)
|
|
||||||
]
|
|
||||||
|
|
||||||
response = self.llm.invoke(messages)
|
|
||||||
|
|
||||||
# Парсим JSON ответ
|
|
||||||
try:
|
|
||||||
content = response.content
|
|
||||||
if "```json" in content:
|
|
||||||
content = content.split("```json")[1].split("```")[0]
|
|
||||||
elif "```" in content:
|
|
||||||
content = content.split("```")[1].split("```")[0]
|
|
||||||
|
|
||||||
data = json.loads(content.strip())
|
|
||||||
|
|
||||||
comments = []
|
|
||||||
for comment_data in data.get("comments", []):
|
|
||||||
comments.append(ReviewComment(
|
|
||||||
line=comment_data.get("line"),
|
|
||||||
file=comment_data.get("file"),
|
|
||||||
severity=comment_data.get("severity", "major"),
|
|
||||||
text=comment_data.get("text", "")
|
|
||||||
))
|
|
||||||
|
|
||||||
return ReviewResult(
|
|
||||||
status=data.get("status", "changes_requested"),
|
|
||||||
comments=comments,
|
|
||||||
summary=data.get("summary", "")
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
raise ValueError(f"Failed to parse reviewer response: {e}\nResponse: {response.content}")
|
|
||||||
100
agent/reviewer.py
Normal file
100
agent/reviewer.py
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
from typing import Optional, Dict, Any
|
||||||
|
from langchain_core.messages import HumanMessage, SystemMessage
|
||||||
|
from langchain_community.chat_models import ChatOllama
|
||||||
|
import json
|
||||||
|
from config import MODEL_NAME, TEMPERATURE, LLM_HOST
|
||||||
|
from models import CodeSolution, ReviewResult, ReviewComment, Task
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
class ReviewerAgent:
|
||||||
|
"""Агент-ревьювер: проверяет качество кода"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.llm = ChatOllama(
|
||||||
|
model=MODEL_NAME,
|
||||||
|
temperature=0.1,
|
||||||
|
base_url=LLM_HOST,
|
||||||
|
num_predict=2048
|
||||||
|
)
|
||||||
|
|
||||||
|
def review(self, task: Task, solution: CodeSolution) -> ReviewResult:
|
||||||
|
"""Проверяет код и возвращает замечания"""
|
||||||
|
|
||||||
|
system_prompt = """Ты - строгий ревьювер кода. Твоя задача:
|
||||||
|
1. Проверить соответствие требованиям
|
||||||
|
2. Найти логические ошибки и баги
|
||||||
|
3. Оценить читаемость и стиль кода
|
||||||
|
4. Проверить покрытие тестами
|
||||||
|
5. Найти проблемы безопасности
|
||||||
|
6. Предложить улучшения
|
||||||
|
|
||||||
|
Оценивай код критически. Будь конкретен в замечаниях, указывай файлы и строки.
|
||||||
|
|
||||||
|
Допустимые уровни severity (от критичного к информационному):
|
||||||
|
- critical: блокирующие ошибки (безопасность, падения)
|
||||||
|
- major: серьёзные логические ошибки, неверная работа
|
||||||
|
- minor: стиль, читаемость, мелкие баги
|
||||||
|
- warning: потенциальные проблемы, улучшения, edge cases
|
||||||
|
- info: предложения, рекомендации, необязательные улучшения
|
||||||
|
- suggestion: альтернативные подходы
|
||||||
|
|
||||||
|
Верни JSON с полями:
|
||||||
|
- status: "approved", "changes_requested", или "rejected"
|
||||||
|
- comments: массив объектов с полями: file, line, severity, text
|
||||||
|
- summary: краткое резюме ревью
|
||||||
|
"""
|
||||||
|
# Формируем представление кода для ревью
|
||||||
|
code_for_review = ""
|
||||||
|
for filename, content in solution.files.items():
|
||||||
|
code_for_review += f"\n--- {filename} ---\n{content}\n"
|
||||||
|
|
||||||
|
if solution.tests:
|
||||||
|
code_for_review += "\n--- TESTS ---\n"
|
||||||
|
for testname, test_content in solution.tests.items():
|
||||||
|
code_for_review += f"\n--- {testname} ---\n{test_content}\n"
|
||||||
|
|
||||||
|
user_prompt = f"""
|
||||||
|
Требование: {task.requirement}
|
||||||
|
|
||||||
|
Код на ревью:
|
||||||
|
{code_for_review}
|
||||||
|
|
||||||
|
Описание решения разработчика: {solution.description}
|
||||||
|
|
||||||
|
Проведи ревью. Будь строг, но справедлив.
|
||||||
|
"""
|
||||||
|
|
||||||
|
messages = [
|
||||||
|
SystemMessage(content=system_prompt),
|
||||||
|
HumanMessage(content=user_prompt)
|
||||||
|
]
|
||||||
|
|
||||||
|
response = self.llm.invoke(messages)
|
||||||
|
|
||||||
|
# Парсим JSON ответ
|
||||||
|
try:
|
||||||
|
content = response.content
|
||||||
|
if "```json" in content:
|
||||||
|
content = content.split("```json")[1].split("```")[0]
|
||||||
|
elif "```" in content:
|
||||||
|
content = content.split("```")[1].split("```")[0]
|
||||||
|
|
||||||
|
data = json.loads(content.strip())
|
||||||
|
|
||||||
|
comments = []
|
||||||
|
for comment_data in data.get("comments", []):
|
||||||
|
comments.append(ReviewComment(
|
||||||
|
line=comment_data.get("line"),
|
||||||
|
file=comment_data.get("file"),
|
||||||
|
severity=comment_data.get("severity", "major"),
|
||||||
|
text=comment_data.get("text", "")
|
||||||
|
))
|
||||||
|
|
||||||
|
return ReviewResult(
|
||||||
|
status=data.get("status", "changes_requested"),
|
||||||
|
comments=comments,
|
||||||
|
summary=data.get("summary", "")
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
raise ValueError(f"Failed to parse reviewer response: {e}\nResponse: {response.content}")
|
||||||
@@ -8,7 +8,7 @@ load_dotenv()
|
|||||||
# OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
|
# OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
|
||||||
LLM_HOST = "http://192.168.212.100:11434"
|
LLM_HOST = "http://192.168.212.100:11434"
|
||||||
MODEL_NAME = "qwen3.5-16k:latest" # или "gpt-3.5-turbo" для тестов
|
MODEL_NAME = "qwen3.5-16k:latest" # или "gpt-3.5-turbo" для тестов
|
||||||
TEMPERATURE = 0.3
|
TEMPERATURE = 0.2
|
||||||
MAX_ITERATIONS = 3
|
MAX_ITERATIONS = 3
|
||||||
|
|
||||||
# Конфигурация системы
|
# Конфигурация системы
|
||||||
|
|||||||
21
main.py
21
main.py
@@ -15,7 +15,7 @@ def load_requirement_from_file(filepath: str) -> str:
|
|||||||
return f.read()
|
return f.read()
|
||||||
|
|
||||||
def save_report(task, output_file: Optional[str] = None):
|
def save_report(task, output_file: Optional[str] = None):
|
||||||
"""Сохраняет детальный отчет о выполнении"""
|
"""Сохраняет детальный отчет с историей всех итераций"""
|
||||||
report = {
|
report = {
|
||||||
"task_id": task.id,
|
"task_id": task.id,
|
||||||
"timestamp": datetime.now().isoformat(),
|
"timestamp": datetime.now().isoformat(),
|
||||||
@@ -24,9 +24,23 @@ def save_report(task, output_file: Optional[str] = None):
|
|||||||
"iterations": task.iteration,
|
"iterations": task.iteration,
|
||||||
"final_review": None,
|
"final_review": None,
|
||||||
"error_log": task.error_log,
|
"error_log": task.error_log,
|
||||||
"files_generated": []
|
"files_generated": [],
|
||||||
|
"history": [] # <-- добавляем историю
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Добавляем историю итераций
|
||||||
|
for record in task.history:
|
||||||
|
report["history"].append({
|
||||||
|
"iteration": record.iteration,
|
||||||
|
"timestamp": record.timestamp,
|
||||||
|
"agent": record.agent,
|
||||||
|
"action": record.action,
|
||||||
|
"input_summary": record.input_summary,
|
||||||
|
"output_summary": record.output_summary,
|
||||||
|
"details": record.details,
|
||||||
|
"error": record.error
|
||||||
|
})
|
||||||
|
|
||||||
if task.last_review:
|
if task.last_review:
|
||||||
report["final_review"] = {
|
report["final_review"] = {
|
||||||
"status": task.last_review.status,
|
"status": task.last_review.status,
|
||||||
@@ -48,14 +62,13 @@ def save_report(task, output_file: Optional[str] = None):
|
|||||||
report["tests_generated"] = list(task.solution.tests.keys())
|
report["tests_generated"] = list(task.solution.tests.keys())
|
||||||
report["description"] = task.solution.description
|
report["description"] = task.solution.description
|
||||||
|
|
||||||
# Сохраняем в файл
|
|
||||||
if not output_file:
|
if not output_file:
|
||||||
output_file = f"report_{task.id}.json"
|
output_file = f"report_{task.id}.json"
|
||||||
|
|
||||||
with open(output_file, 'w', encoding='utf-8') as f:
|
with open(output_file, 'w', encoding='utf-8') as f:
|
||||||
json.dump(report, f, indent=2, ensure_ascii=False)
|
json.dump(report, f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
print(f"\n📄 Отчет сохранен: {output_file}")
|
print(f"\n📄 Report saved: {output_file}")
|
||||||
return report
|
return report
|
||||||
|
|
||||||
def interactive_mode():
|
def interactive_mode():
|
||||||
|
|||||||
16
models.py
16
models.py
@@ -15,7 +15,7 @@ class TaskStatus(str, Enum):
|
|||||||
class ReviewComment(BaseModel):
|
class ReviewComment(BaseModel):
|
||||||
line: Optional[int] = None
|
line: Optional[int] = None
|
||||||
file: Optional[str] = None
|
file: Optional[str] = None
|
||||||
severity: Literal["critical", "major", "minor", "suggestion"] = "major"
|
severity: Literal["critical", "major", "minor", "suggestion", "warning", "info"] = "major"
|
||||||
text: str
|
text: str
|
||||||
|
|
||||||
class ReviewResult(BaseModel):
|
class ReviewResult(BaseModel):
|
||||||
@@ -28,6 +28,17 @@ class CodeSolution(BaseModel):
|
|||||||
description: str
|
description: str
|
||||||
tests: Dict[str, str] = Field(default_factory=dict) # test_filename -> content
|
tests: Dict[str, str] = Field(default_factory=dict) # test_filename -> content
|
||||||
|
|
||||||
|
class IterationRecord(BaseModel):
|
||||||
|
"""Запись одной итерации"""
|
||||||
|
iteration: int
|
||||||
|
timestamp: str
|
||||||
|
agent: Literal["developer", "reviewer"]
|
||||||
|
action: str # "start", "complete", "error"
|
||||||
|
input_summary: Optional[str] = None
|
||||||
|
output_summary: Optional[str] = None
|
||||||
|
details: Dict[str, Any] = Field(default_factory=dict)
|
||||||
|
error: Optional[str] = None
|
||||||
|
|
||||||
class Task(BaseModel):
|
class Task(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
requirement: str
|
requirement: str
|
||||||
@@ -35,4 +46,5 @@ class Task(BaseModel):
|
|||||||
iteration: int = 0
|
iteration: int = 0
|
||||||
solution: Optional[CodeSolution] = None
|
solution: Optional[CodeSolution] = None
|
||||||
last_review: Optional[ReviewResult] = None
|
last_review: Optional[ReviewResult] = None
|
||||||
error_log: List[str] = Field(default_factory=list)
|
error_log: List[str] = Field(default_factory=list)
|
||||||
|
history: List[IterationRecord] = Field(default_factory=list) # <-- новая история
|
||||||
@@ -4,8 +4,8 @@ from langgraph.graph import StateGraph, END
|
|||||||
from langgraph.checkpoint.memory import MemorySaver
|
from langgraph.checkpoint.memory import MemorySaver
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from models import Task, TaskStatus, CodeSolution, ReviewResult
|
from models import Task, TaskStatus, CodeSolution, ReviewResult, IterationRecord
|
||||||
from agents import DeveloperAgent, ReviewerAgent
|
from agent import DeveloperAgent, ReviewerAgent
|
||||||
from config import MAX_REVIEW_RETRIES
|
from config import MAX_REVIEW_RETRIES
|
||||||
|
|
||||||
class Orchestrator:
|
class Orchestrator:
|
||||||
@@ -20,6 +20,22 @@ class Orchestrator:
|
|||||||
self.workflow = self._build_workflow()
|
self.workflow = self._build_workflow()
|
||||||
self.checkpointer = MemorySaver()
|
self.checkpointer = MemorySaver()
|
||||||
self.app = self.workflow.compile()
|
self.app = self.workflow.compile()
|
||||||
|
|
||||||
|
def _add_history(self, task: Task, agent: str, action: str,
|
||||||
|
input_summary: str = None, output_summary: str = None,
|
||||||
|
details: Dict = None, error: str = None):
|
||||||
|
"""Добавляет запись в историю задачи"""
|
||||||
|
record = IterationRecord(
|
||||||
|
iteration=task.iteration,
|
||||||
|
timestamp=datetime.now().isoformat(),
|
||||||
|
agent=agent,
|
||||||
|
action=action,
|
||||||
|
input_summary=input_summary,
|
||||||
|
output_summary=output_summary,
|
||||||
|
details=details or {},
|
||||||
|
error=error
|
||||||
|
)
|
||||||
|
task.history.append(record)
|
||||||
|
|
||||||
def _build_workflow(self) -> StateGraph:
|
def _build_workflow(self) -> StateGraph:
|
||||||
"""Строит граф процесса разработки"""
|
"""Строит граф процесса разработки"""
|
||||||
@@ -57,6 +73,13 @@ class Orchestrator:
|
|||||||
task = state["task"]
|
task = state["task"]
|
||||||
print(f"\n🔧 [Iteration {task.iteration}] Разработчик начал работу...")
|
print(f"\n🔧 [Iteration {task.iteration}] Разработчик начал работу...")
|
||||||
|
|
||||||
|
# Логируем начало работы разработчика
|
||||||
|
self._add_history(
|
||||||
|
task, "developer", "start",
|
||||||
|
input_summary=f"Requirement: {task.requirement[:100]}...",
|
||||||
|
details={"iteration": task.iteration, "previous_comments_count": len(task.last_review.comments) if task.last_review else 0}
|
||||||
|
)
|
||||||
|
|
||||||
# Получаем замечания с предыдущего ревью
|
# Получаем замечания с предыдущего ревью
|
||||||
previous_comments = []
|
previous_comments = []
|
||||||
if task.last_review and task.last_review.comments:
|
if task.last_review and task.last_review.comments:
|
||||||
@@ -71,6 +94,13 @@ class Orchestrator:
|
|||||||
task.solution = solution
|
task.solution = solution
|
||||||
task.status = TaskStatus.REVIEW
|
task.status = TaskStatus.REVIEW
|
||||||
|
|
||||||
|
# Логируем успех
|
||||||
|
self._add_history(
|
||||||
|
task, "developer", "complete",
|
||||||
|
output_summary=f"Generated {len(solution.files)} file(s), {len(solution.tests)} test(s)",
|
||||||
|
details={"files": list(solution.files.keys()), "tests": list(solution.tests.keys())}
|
||||||
|
)
|
||||||
|
|
||||||
print(f"✅ Разработчик создал решение: {len(solution.files)} файлов")
|
print(f"✅ Разработчик создал решение: {len(solution.files)} файлов")
|
||||||
for filename in solution.files:
|
for filename in solution.files:
|
||||||
print(f" - {filename}")
|
print(f" - {filename}")
|
||||||
@@ -78,6 +108,13 @@ class Orchestrator:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
task.status = TaskStatus.FAILED
|
task.status = TaskStatus.FAILED
|
||||||
task.error_log.append(f"Developer error: {str(e)}")
|
task.error_log.append(f"Developer error: {str(e)}")
|
||||||
|
|
||||||
|
# Логируем ошибку
|
||||||
|
self._add_history(
|
||||||
|
task, "developer", "error",
|
||||||
|
error=error_msg,
|
||||||
|
details={"exception": str(e)}
|
||||||
|
)
|
||||||
print(f"❌ Ошибка разработчика: {e}")
|
print(f"❌ Ошибка разработчика: {e}")
|
||||||
|
|
||||||
return {"task": task}
|
return {"task": task}
|
||||||
@@ -87,9 +124,18 @@ class Orchestrator:
|
|||||||
task = state["task"]
|
task = state["task"]
|
||||||
print(f"\n🔍 [Iteration {task.iteration}] Ревьювер проверяет код...")
|
print(f"\n🔍 [Iteration {task.iteration}] Ревьювер проверяет код...")
|
||||||
|
|
||||||
|
# Логируем начало ревью
|
||||||
|
self._add_history(
|
||||||
|
task, "reviewer", "start",
|
||||||
|
input_summary=f"Reviewing {len(task.solution.files)} file(s)" if task.solution else "No solution",
|
||||||
|
details={"iteration": task.iteration}
|
||||||
|
)
|
||||||
|
|
||||||
if not task.solution:
|
if not task.solution:
|
||||||
task.status = TaskStatus.FAILED
|
task.status = TaskStatus.FAILED
|
||||||
task.error_log.append("No solution to review")
|
error_msg = "No solution to review"
|
||||||
|
task.error_log.append(error_msg)
|
||||||
|
self._add_history(task, "reviewer", "error", error=error_msg)
|
||||||
return {"task": task}
|
return {"task": task}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -98,6 +144,17 @@ class Orchestrator:
|
|||||||
task.last_review = review
|
task.last_review = review
|
||||||
task.iteration += 1
|
task.iteration += 1
|
||||||
|
|
||||||
|
# Логируем результат ревью
|
||||||
|
self._add_history(
|
||||||
|
task, "reviewer", "complete",
|
||||||
|
output_summary=f"Verdict: {review.status}, Comments: {len(review.comments)}",
|
||||||
|
details={
|
||||||
|
"verdict": review.status,
|
||||||
|
"comments_count": len(review.comments),
|
||||||
|
"summary": review.summary
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
print(f"📊 Вердикт: {review.status}")
|
print(f"📊 Вердикт: {review.status}")
|
||||||
if review.comments:
|
if review.comments:
|
||||||
print(f" Найдено замечаний: {len(review.comments)}")
|
print(f" Найдено замечаний: {len(review.comments)}")
|
||||||
@@ -117,8 +174,9 @@ class Orchestrator:
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
task.status = TaskStatus.FAILED
|
task.status = TaskStatus.FAILED
|
||||||
task.error_log.append(f"Reviewer error: {str(e)}")
|
error_msg = f"Reviewer error: {str(e)}"
|
||||||
print(f"❌ Ошибка ревьювера: {e}")
|
task.error_log.append(error_msg)
|
||||||
|
self._add_history(task, "reviewer", "error", error=error_msg, details={"exception": str(e)})
|
||||||
|
|
||||||
return {"task": task}
|
return {"task": task}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
langgraph>=0.3.0
|
langgraph>=0.3.0
|
||||||
langchain>=0.2.0
|
langchain>=0.2.0
|
||||||
langchain-openai>=0.1.0
|
|
||||||
langchain-community>=0.0.16
|
langchain-community>=0.0.16
|
||||||
python-dotenv>=1.0.0
|
python-dotenv>=1.0.0
|
||||||
pydantic>=2.0.0
|
pydantic>=2.0.0
|
||||||
|
|||||||
Reference in New Issue
Block a user