Skip to content

Commit bfb3d66

Browse files
authored
Merge pull request #33 from ks6088ts-labs/copilot/fix-32
Add LangGraph-based Agent API with modular architecture and CLI tool
2 parents f7d82f2 + 8e96770 commit bfb3d66

File tree

17 files changed

+1188
-121
lines changed

17 files changed

+1188
-121
lines changed

docs/index.md

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -204,17 +204,69 @@ az resource update \
204204

205205
```bash
206206
# エージェントを作成
207-
python scripts/agents.py create-agent "研究アシスタント" --description "研究をサポートするAIアシスタント" --instructions "あなたは研究者をサポートするAIアシスタントです。質問に対して詳細で正確な回答を提供してください。"
207+
uv run python scripts/agents_azure_ai_foundry.py create-agent "研究アシスタント" --description "研究をサポートするAIアシスタント" --instructions "あなたは研究者をサポートするAIアシスタントです。質問に対して詳細で正確な回答を提供してください。"
208208

209209
# エージェント一覧を取得
210-
python scripts/agents.py list-agents
210+
uv run python scripts/agents_azure_ai_foundry.py list-agents
211211

212212
# エージェントの詳細を取得
213-
python scripts/agents.py get-agent <agent_id>
213+
uv run python scripts/agents_azure_ai_foundry.py get-agent <agent_id>
214214

215215
# エージェントとチャット
216-
python scripts/agents.py chat <agent_id> "機械学習の最新トレンドについて教えてください"
216+
uv run python scripts/agents_azure_ai_foundry.py chat <agent_id> "機械学習の最新トレンドについて教えてください"
217217

218218
# エージェントを削除
219-
python scripts/agents.py delete-agent <agent_id>
219+
uv run python scripts/agents_azure_ai_foundry.py delete-agent <agent_id>
220+
```
221+
222+
### LangGraph Agent
223+
224+
LangGraph ベースのエージェント API を使用した対話型 AI アシスタント。ツール呼び出し機能を持つシンプルなエージェントワークフローを実装しています。
225+
226+
#### CLI 実行例
227+
228+
```bash
229+
# ヘルプ
230+
uv run python scripts/agents_langgraph.py --help
231+
232+
# エージェントとチャット
233+
uv run python scripts/agents_langgraph.py chat "こんにちは!今何時ですか?"
234+
235+
# スレッドIDを指定してチャット(会話の継続)
236+
uv run python scripts/agents_langgraph.py chat "前回の続きを教えてください" --thread-id "12345-67890-abcdef"
237+
238+
# 詳細情報付きでチャット
239+
uv run python scripts/agents_langgraph.py chat "2 + 2 × 3 を計算してください" --verbose
240+
241+
# 対話モード
242+
uv run python scripts/agents_langgraph.py interactive
243+
244+
# 利用可能なツール一覧
245+
uv run python scripts/agents_langgraph.py tools
246+
247+
# デモモード(サンプル質問のテスト)
248+
uv run python scripts/agents_langgraph.py demo
249+
```
250+
251+
#### API エンドポイント
252+
253+
```bash
254+
# FastAPIサーバーを起動
255+
make dev
256+
257+
# LangGraphエージェントとチャット
258+
curl -X POST "http://localhost:8000/agents/langgraph/chat" \
259+
-H "Content-Type: application/json" \
260+
-d '{"message": "こんにちは!"}'
261+
262+
# ストリーミングチャット
263+
curl -X POST "http://localhost:8000/agents/langgraph/chat/stream" \
264+
-H "Content-Type: application/json" \
265+
-d '{"message": "長い回答をお願いします"}'
266+
267+
# 利用可能なツール一覧
268+
curl -X GET "http://localhost:8000/agents/langgraph/tools"
269+
270+
# ヘルスチェック
271+
curl -X GET "http://localhost:8000/agents/langgraph/health"
220272
```

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ dependencies = [
1515
"fastapi[standard]>=0.115.12",
1616
"langchain-community>=0.3.27",
1717
"langchain-openai>=0.3.27",
18+
"langgraph>=0.2.90",
1819
"msgraph-sdk>=1.37.0",
1920
"opentelemetry-instrumentation-fastapi>=0.52b1",
2021
"pydantic-settings>=2.10.1",

scripts/agents.py renamed to scripts/agents_azure_ai_foundry.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
#!/usr/bin/env python
2-
# filepath: /home/runner/work/template-fastapi/template-fastapi/scripts/agents.py
3-
41
import json
52

63
import typer

scripts/agents_langgraph.py

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
"""LangGraph Agent CLI tool."""
2+
3+
import typer
4+
from rich.console import Console
5+
from rich.markdown import Markdown
6+
from rich.panel import Panel
7+
8+
from template_fastapi.internals.langgraph.agents import LangGraphAgent
9+
from template_fastapi.internals.langgraph.tools import get_tools
10+
11+
app = typer.Typer()
12+
console = Console()
13+
14+
15+
@app.command()
16+
def chat(
17+
message: str = typer.Argument(..., help="メッセージ"),
18+
thread_id: str | None = typer.Option(None, "--thread-id", "-t", help="スレッドID(会話の継続用)"),
19+
verbose: bool = typer.Option(False, "--verbose", "-v", help="詳細な情報を表示"),
20+
):
21+
"""LangGraphエージェントとチャットする"""
22+
console.print("[bold green]LangGraphエージェントとチャットします[/bold green]")
23+
console.print(f"メッセージ: {message}")
24+
25+
if thread_id:
26+
console.print(f"スレッドID: {thread_id}")
27+
else:
28+
console.print("新しいスレッドを作成します")
29+
30+
try:
31+
# Initialize the LangGraph agent
32+
agent = LangGraphAgent()
33+
34+
# Show loading message
35+
with console.status("[bold green]エージェントが応答を生成中...", spinner="dots"):
36+
result = agent.chat(message=message, thread_id=thread_id)
37+
38+
# Display results
39+
console.print("\n" + "=" * 50)
40+
console.print("[bold blue]チャット結果[/bold blue]")
41+
console.print("=" * 50)
42+
43+
# Display user message
44+
user_panel = Panel(message, title="[bold cyan]あなた[/bold cyan]", border_style="cyan")
45+
console.print(user_panel)
46+
47+
# Display agent response with markdown rendering
48+
response_content = result["response"]
49+
if response_content:
50+
try:
51+
# Try to render as markdown for better formatting
52+
markdown = Markdown(response_content)
53+
agent_panel = Panel(
54+
markdown, title="[bold green]LangGraphエージェント[/bold green]", border_style="green"
55+
)
56+
except Exception:
57+
# Fallback to plain text
58+
agent_panel = Panel(
59+
response_content, title="[bold green]LangGraphエージェント[/bold green]", border_style="green"
60+
)
61+
console.print(agent_panel)
62+
63+
# Display metadata
64+
if verbose:
65+
console.print("\n[bold yellow]メタデータ[/bold yellow]:")
66+
console.print(f"スレッドID: {result['thread_id']}")
67+
console.print(f"作成日時: {result['created_at']}")
68+
console.print(f"ステップ数: {result.get('step_count', 0)}")
69+
70+
if result.get("tools_used"):
71+
console.print(f"使用ツール: {', '.join(result['tools_used'])}")
72+
else:
73+
console.print("使用ツール: なし")
74+
else:
75+
console.print(f"\n[dim]スレッドID: {result['thread_id']}[/dim]")
76+
if result.get("tools_used"):
77+
console.print(f"[dim]使用ツール: {', '.join(result['tools_used'])}[/dim]")
78+
79+
except Exception as e:
80+
console.print(f"❌ [bold red]エラー[/bold red]: {str(e)}")
81+
82+
83+
@app.command()
84+
def interactive():
85+
"""対話モードでLangGraphエージェントとチャットする"""
86+
console.print("[bold green]LangGraphエージェント対話モード[/bold green]")
87+
console.print("終了するには 'exit', 'quit', または 'bye' と入力してください\n")
88+
89+
agent = LangGraphAgent()
90+
thread_id = None
91+
92+
while True:
93+
try:
94+
# Get user input
95+
user_input = typer.prompt("あなた")
96+
97+
# Check for exit commands
98+
if user_input.lower() in ["exit", "quit", "bye", "終了"]:
99+
console.print("[yellow]対話を終了します。ありがとうございました![/yellow]")
100+
break
101+
102+
# Process the message
103+
with console.status("[bold green]応答を生成中...", spinner="dots"):
104+
result = agent.chat(message=user_input, thread_id=thread_id)
105+
106+
# Update thread_id for conversation continuity
107+
thread_id = result["thread_id"]
108+
109+
# Display agent response
110+
response_panel = Panel(
111+
Markdown(result["response"]) if result["response"] else "応答がありません",
112+
title="[bold green]エージェント[/bold green]",
113+
border_style="green",
114+
)
115+
console.print(response_panel)
116+
117+
# Show tools used if any
118+
if result.get("tools_used"):
119+
console.print(f"[dim]使用ツール: {', '.join(result['tools_used'])}[/dim]")
120+
121+
console.print() # Add spacing
122+
123+
except KeyboardInterrupt:
124+
console.print("\n[yellow]対話を終了します[/yellow]")
125+
break
126+
except Exception as e:
127+
console.print(f"❌ [bold red]エラー[/bold red]: {str(e)}")
128+
129+
130+
@app.command()
131+
def tools():
132+
"""利用可能なツールの一覧を表示する"""
133+
console.print("[bold green]利用可能なツール一覧[/bold green]")
134+
135+
try:
136+
available_tools = get_tools()
137+
138+
if not available_tools:
139+
console.print("[yellow]利用可能なツールがありません[/yellow]")
140+
return
141+
142+
console.print(f"\n[bold blue]合計 {len(available_tools)} 個のツールが利用可能です[/bold blue]\n")
143+
144+
for i, tool in enumerate(available_tools, 1):
145+
tool_info = f"""
146+
**名前:** {tool.name}
147+
**説明:** {tool.description}
148+
"""
149+
if hasattr(tool, "args_schema") and tool.args_schema:
150+
try:
151+
schema = tool.args_schema.model_json_schema()
152+
if "properties" in schema:
153+
tool_info += f"**パラメータ:** {', '.join(schema['properties'].keys())}"
154+
except Exception:
155+
pass
156+
157+
panel = Panel(Markdown(tool_info), title=f"[bold cyan]ツール {i}[/bold cyan]", border_style="cyan")
158+
console.print(panel)
159+
160+
except Exception as e:
161+
console.print(f"❌ [bold red]エラー[/bold red]: {str(e)}")
162+
163+
164+
@app.command()
165+
def demo():
166+
"""デモンストレーション用のサンプルチャット"""
167+
console.print("[bold green]LangGraphエージェント デモモード[/bold green]")
168+
console.print("いくつかのサンプル質問でエージェントをテストします\n")
169+
170+
sample_queries = [
171+
"こんにちは!今何時ですか?",
172+
"2 + 2 × 3 を計算してください",
173+
"Pythonについて検索してください",
174+
]
175+
176+
agent = LangGraphAgent()
177+
178+
for i, query in enumerate(sample_queries, 1):
179+
console.print(f"[bold yellow]サンプル質問 {i}:[/bold yellow] {query}")
180+
181+
try:
182+
with console.status(f"[bold green]質問 {i} を処理中...", spinner="dots"):
183+
result = agent.chat(message=query)
184+
185+
response_panel = Panel(
186+
Markdown(result["response"]) if result["response"] else "応答がありません",
187+
title="[bold green]エージェントの応答[/bold green]",
188+
border_style="green",
189+
)
190+
console.print(response_panel)
191+
192+
if result.get("tools_used"):
193+
console.print(f"[dim]使用ツール: {', '.join(result['tools_used'])}[/dim]")
194+
195+
console.print() # Add spacing
196+
197+
except Exception as e:
198+
console.print(f"❌ [bold red]エラー[/bold red]: {str(e)}")
199+
console.print()
200+
201+
202+
if __name__ == "__main__":
203+
app()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Core module for template_fastapi."""
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""LangGraph components for agent implementation."""
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
"""Main LangGraph agent interface."""
2+
3+
import uuid
4+
from datetime import datetime
5+
from typing import Any
6+
7+
from langchain_core.messages import HumanMessage
8+
9+
from .graphs import get_compiled_graph
10+
from .states import AgentState
11+
12+
13+
class LangGraphAgent:
14+
"""Main interface for LangGraph agent operations."""
15+
16+
def __init__(self):
17+
self.graph = get_compiled_graph()
18+
19+
def chat(self, message: str, thread_id: str | None = None) -> dict[str, Any]:
20+
"""
21+
Chat with the LangGraph agent.
22+
23+
Args:
24+
message: User message
25+
thread_id: Optional thread ID for conversation continuity
26+
27+
Returns:
28+
Chat response with metadata
29+
"""
30+
# Generate thread ID if not provided
31+
if thread_id is None:
32+
thread_id = str(uuid.uuid4())
33+
34+
# Create initial state
35+
initial_state = AgentState(
36+
messages=[HumanMessage(content=message)],
37+
thread_id=thread_id,
38+
tools_used=[],
39+
step_count=0,
40+
)
41+
42+
# Run the graph
43+
config = {"configurable": {"thread_id": thread_id}}
44+
result = self.graph.invoke(initial_state, config=config)
45+
46+
# Extract the final response
47+
final_message = result["messages"][-1]
48+
response_content = final_message.content if hasattr(final_message, "content") else str(final_message)
49+
50+
return {
51+
"message": message,
52+
"response": response_content,
53+
"thread_id": thread_id,
54+
"tools_used": result.get("tools_used", []),
55+
"created_at": datetime.now().isoformat(),
56+
"step_count": result.get("step_count", 0),
57+
}
58+
59+
def stream_chat(self, message: str, thread_id: str | None = None):
60+
"""
61+
Stream chat with the LangGraph agent.
62+
63+
Args:
64+
message: User message
65+
thread_id: Optional thread ID for conversation continuity
66+
67+
Yields:
68+
Streaming responses from the agent
69+
"""
70+
# Generate thread ID if not provided
71+
if thread_id is None:
72+
thread_id = str(uuid.uuid4())
73+
74+
# Create initial state
75+
initial_state = AgentState(
76+
messages=[HumanMessage(content=message)],
77+
thread_id=thread_id,
78+
tools_used=[],
79+
step_count=0,
80+
)
81+
82+
# Stream the graph execution
83+
config = {"configurable": {"thread_id": thread_id}}
84+
yield from self.graph.stream(initial_state, config=config)

0 commit comments

Comments
 (0)