10.2.3.1. チャットサンプル¶
JavaEE開発モデルのAPI「ChatAction」を使った基本的な実装例を紹介します。
コラム
「ChatAction」のAPI仕様は以下を参考にしてください:
コラム
このサンプルは「Web API Maker」を利用しています。
ファクトリクラスおよびパッケージ登録ファイルの説明は割愛しています。
「Web API Maker」の詳細については「Web API Maker プログラミングガイド」を参考にしてください。
10.2.3.1.1. テキスト入力サンプル¶
10.2.3.1.1.1. 非ストリーム形式の実装例¶
import java.util.List;
import com.fasterxml.jackson.databind.ObjectMapper;
import jp.co.intra_mart.foundation.copilot.action.ActionFactory;
import jp.co.intra_mart.foundation.copilot.action.chat.ChatAction;
import jp.co.intra_mart.foundation.copilot.action.chat.ChatMessage;
import jp.co.intra_mart.foundation.copilot.action.chat.ChatOption;
import jp.co.intra_mart.foundation.web_api_maker.annotation.IMAuthentication;
import jp.co.intra_mart.foundation.web_api_maker.annotation.POST;
import jp.co.intra_mart.foundation.web_api_maker.annotation.Parameter;
import jp.co.intra_mart.foundation.web_api_maker.annotation.Path;
import jp.co.intra_mart.foundation.web_api_maker.annotation.Required;
/**
* チャット呼び出しのサンプルです。
*
* <pre>
* 使用例:
* POST /sample/chat
* Body:
* {
* "userPrompt": "最近の政治関連ニュースで、気になる話題はありますか?"
* }
* </pre>
*/
@IMAuthentication
public class ChatSample {
private static final String SYSTEM_PROMPT = "この会話は、ビジネスパーソン同士の自然なやりとりとして応答してください。堅すぎず、親しみやすいトーンでお願いします。";
/**
* ユーザから受け取ったプロンプトに対して、チャット応答を生成して返却します。
*
* @param userPrompt ユーザからのプロンプト
* @return チャット応答
* @throws Exception 応答生成中にエラーが発生した場合
*/
@Path("/sample/chat")
@POST
public String chat(@Required @Parameter(name = "userPrompt") String userPrompt) throws Exception {
// チャットの履歴を含むメッセージ一覧を作成する
// 最初に、アシスタントの振る舞いを定義する「システムプロンプト」を追加
// その後、ユーザとアシスタントの過去の会話を順番に追加
// 最後に、ユーザからの新しい質問(userPrompt)を追加して、会話の流れを構築する
// @formatter:off
final List<ChatMessage> messages = ChatMessage.builder()
.newMessage().withRole("system").addTextContent(SYSTEM_PROMPT)
.newMessage().withRole("user").addTextContent("最近の業界ニュースで気になることってありました?")
.newMessage().withRole("assistant").addTextContent("そうですね、最近では新しい規制案の話題が注目されています。中小企業への影響が大きそうで、社内でも話題になっていました。")
.newMessage().withRole("user").addTextContent("他に注目していることはありますか?")
.newMessage().withRole("assistant").addTextContent("最近は人材の流動性が高まっているという話もよく聞きます。特に若手の転職が増えており、採用や育成の方針を見直す企業も増えているようです。")
.newMessage().withRole("user").addTextContent(userPrompt)
.build();
// @formatter:on
// チャット実行
final ActionFactory factory = ActionFactory.getFactory();
final ChatAction action = factory.getChatAction();
final ChatOption option = ChatOption.builder().build();
final List<ChatMessage> result = action.execute(messages, option);
if (result != null && !result.isEmpty()) {
// チャットメッセージのリストが存在し、空でない場合は、
// 最後のメッセージを取得して JSON 形式の文字列に変換する
final ChatMessage lastMessage = result.get(result.size() - 1);
// コンテンツフィルタ: AIサービスの不適切コンテンツ検出機能
// 入力違反時はAPI実行時にエラーが発生、出力違反時はcontentFilterプロパティで検出可能
if (lastMessage.getContentFilter() != null && lastMessage.getContentFilter().isOutputBlocked()) {
// 必要に応じて出力違反時の処理を記載
}
final String json = new ObjectMapper().writeValueAsString(lastMessage);
return json;
} else {
// メッセージが存在しない場合は、空の JSON オブジェクトを返す
return "{}";
}
}
}
10.2.3.1.1.2. ストリーム形式の実装例¶
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import jp.co.intra_mart.foundation.copilot.action.ActionFactory;
import jp.co.intra_mart.foundation.copilot.action.chat.ChatAction;
import jp.co.intra_mart.foundation.copilot.action.chat.ChatMessage;
import jp.co.intra_mart.foundation.copilot.action.chat.ChatOption;
import jp.co.intra_mart.foundation.copilot.common.ContentFilterInfo;
import jp.co.intra_mart.foundation.web_api_maker.annotation.IMAuthentication;
import jp.co.intra_mart.foundation.web_api_maker.annotation.POST;
import jp.co.intra_mart.foundation.web_api_maker.annotation.Parameter;
import jp.co.intra_mart.foundation.web_api_maker.annotation.Path;
import jp.co.intra_mart.foundation.web_api_maker.annotation.PreventWritingResponse;
import jp.co.intra_mart.foundation.web_api_maker.annotation.Required;
/**
* チャットストリーミング呼び出しのサンプルです。
*
* <p>
* Content-Type: text/event-stream を使用し、data: プレフィックス付きでチャンクデータを送信します。
* </p>
*
* <pre>
* 使用例:
* POST /sample/chat/stream
* Body:
* {
* "userPrompt": "最近の政治関連ニュースで、気になる話題はありますか?"
* }
* </pre>
*/
@IMAuthentication
public class ChatStreamSample {
private static final String SYSTEM_PROMPT = "この会話は、ビジネスパーソン同士の自然なやりとりとして応答してください。堅すぎず、親しみやすいトーンでお願いします。";
/**
* チャットのストリーミングで送信される1チャンク分のデータを表すクラス(模擬実装)
*/
public class ChunkMessage {
private String role;
private String content;
public ChunkMessage(String role, String content) {
this.role = role;
this.content = content;
}
public String getRole() {
return role;
}
public String getContent() {
return content;
}
public void setRole(String role) {
this.role = role;
}
public void setContent(String content) {
this.content = content;
}
}
/**
* クライアントにデータを送信するためのハンドラクラス(模擬実装)
*/
public class StreamHandler {
private static final String DATA_PREFIX = "data: ";
private static final String EVENT_ERROR = "event: error";
private static final String EVENT_CONTENT_FILTERED = "event: content_filtered";
private static final String LINE_BREAK = "\n";
private static final String DOUBLE_LINE_BREAK = "\n\n";
private final PrintWriter writer;
public StreamHandler(HttpServletResponse response) throws IOException {
this.writer = response.getWriter();
}
/**
* 通常のチャンクデータを送信
*/
public void send(ChunkMessage chunk) {
writer.write(DATA_PREFIX + chunk.getContent() + DOUBLE_LINE_BREAK);
writer.flush();
}
/**
* エラーイベントを送信
*/
public void sendError(String errorMessage) {
writer.write(EVENT_ERROR + LINE_BREAK);
writer.write(DATA_PREFIX + errorMessage + DOUBLE_LINE_BREAK);
writer.flush();
}
/**
* コンテンツフィルタ情報を送信
*/
public void sendContentFilter(final ContentFilterInfo contentFilter) throws IOException {
writer.write(EVENT_CONTENT_FILTERED + LINE_BREAK);
writer.write(DATA_PREFIX + new ObjectMapper().writeValueAsString(contentFilter) + DOUBLE_LINE_BREAK);
writer.flush();
}
}
/**
* クライアントからのプロンプトを受け取り、チャット応答を1文字ずつストリーミング送信します。
*
* @param userPrompt クライアントから送信されたプロンプト(例:「議事録をまとめてください」)
* @param response HTTPレスポンスオブジェクト。SSE形式で出力されます。
* @throws Exception 応答生成中にエラーが発生した場合
*/
@Path("/sample/chat/stream")
@POST
@PreventWritingResponse
public void chatStream(@Required @Parameter(name = "userPrompt") String userPrompt, final HttpServletResponse response) throws Exception {
// レスポンス設定
response.setContentType("text/event-stream");
response.setCharacterEncoding("UTF-8");
// チャットの履歴を含むメッセージ一覧を作成する
// 最初に、アシスタントの振る舞いを定義する「システムプロンプト」を追加
// その後、ユーザとアシスタントの過去の会話を順番に追加
// 最後に、ユーザからの新しい質問(userPrompt)を追加して、会話の流れを構築する
// @formatter:off
final List<ChatMessage> messages = ChatMessage.builder()
.newMessage().withRole("system").addTextContent(SYSTEM_PROMPT)
.newMessage().withRole("user").addTextContent("最近の業界ニュースで気になることってありました?")
.newMessage().withRole("assistant").addTextContent("そうですね、最近では新しい規制案の話題が注目されています。中小企業への影響が大きそうで、社内でも話題になっていました。")
.newMessage().withRole("user").addTextContent("他に注目していることはありますか?")
.newMessage().withRole("assistant").addTextContent("最近は人材の流動性が高まっているという話もよく聞きます。特に若手の転職が増えており、採用や育成の方針を見直す企業も増えているようです。")
.newMessage().withRole("user").addTextContent(userPrompt)
.build();
// @formatter:on
// クライアントにデータを送信するためのハンドラ
StreamHandler handler = new StreamHandler(response);
// チャンクデータの初期化
final ChunkMessage chunkMessage = new ChunkMessage(null, "");
// チャット実行(非同期でチャンクを受け取る)
final ActionFactory factory = ActionFactory.getFactory();
final ChatAction action = factory.getChatAction();
final ChatOption option = ChatOption.builder().build();
action.execute(messages, option, chunk -> {
try {
// コンテンツフィルタ: AIサービスの不適切コンテンツ検出機能
// 入力違反時は通常エラーが発生(一部AIサービス:Amazon Bedrock ではchunk.getContentFilter()で検出)
// 出力違反時はchunk.getContentFilter()で検出可能
if (chunk.getContentFilter() != null) {
// クライアントにフィルタリング情報を送信
handler.sendContentFilter(chunk.getContentFilter());
return;
}
if (null == chunk.getDelta().getContent()) {
return;
}
// チャンクデータを更新
chunkMessage.setContent(chunk.getDelta().getContent());
// クライアントに送信
handler.send(chunkMessage);
} catch (Exception e) {
// エラー発生時にエラーメッセージを送信
handler.sendError("Error sending chunk: " + e.getMessage());
}
});
}
}
10.2.3.1.2. 画像入力サンプル¶
10.2.3.1.2.1. 画像付きの実装例¶
import java.util.List;
import com.fasterxml.jackson.databind.ObjectMapper;
import jp.co.intra_mart.foundation.copilot.action.ActionFactory;
import jp.co.intra_mart.foundation.copilot.action.chat.ChatAction;
import jp.co.intra_mart.foundation.copilot.action.chat.ChatMessage;
import jp.co.intra_mart.foundation.copilot.action.chat.ChatOption;
import jp.co.intra_mart.foundation.service.client.file.PublicStorage;
import jp.co.intra_mart.foundation.web_api_maker.annotation.POST;
import jp.co.intra_mart.foundation.web_api_maker.annotation.IMAuthentication;
import jp.co.intra_mart.foundation.web_api_maker.annotation.Parameter;
import jp.co.intra_mart.foundation.web_api_maker.annotation.Path;
import jp.co.intra_mart.foundation.web_api_maker.annotation.Required;
/**
* 画像を使用したチャット呼び出しのサンプルです。
*
* <pre>
* 使用例:
* POST /sample/analyze/city-crowd
* Body:
* {
* "location": "tokyo-station"
* }
* </pre>
*
*/
@IMAuthentication
public class ChatImageSample {
private static final String SYSTEM_PROMPT = "あなたは都市の監視カメラ画像を分析し、混雑状況を自然な日本語で説明するAIです。指定された画像をもとに、人の数や密度から混雑度を4段階で評価し、簡潔に理由とアドバイスを伝えてください。個人が特定できる情報には触れず、匿名性を保ってください。";
/**
* 指定された場所情報に基づいて、ストレージ上の画像を取得・分析し、その結果に応じたチャット応答を生成して返却します。
*
* @param location 場所
* @return チャット応答
* @throws Exception 応答生成中にエラーが発生した場合
*/
@Path("/sample/analyze/city-crowd")
@POST
public String chat(@Required @Parameter(name = "location") String location) throws Exception {
// tokyo-station.png
final PublicStorage publicStorage = new PublicStorage(location.concat(".png"));
if (!publicStorage.isFile()) {
// 画像ファイルがストレージに存在しない場合は、空の JSON オブジェクトを返す
return "{}";
}
// @formatter:off
final List<ChatMessage> messages = ChatMessage.builder().
newMessage().withRole("system").addTextContent(SYSTEM_PROMPT).
newMessage().withRole("user").addTextContent("次の画像を分析してください。").addImageData(publicStorage.load()).
build();
// @formatter:on
// チャット実行
final ActionFactory factory = ActionFactory.getFactory();
final ChatAction action = factory.getChatAction();
final ChatOption option = ChatOption.builder().build();
final List<ChatMessage> result = action.execute(messages, option);
if (result != null && !result.isEmpty()) {
// チャットメッセージのリストが存在し、空でない場合は、
// 最後のメッセージを取得して JSON 形式の文字列に変換する
final ChatMessage lastMessage = result.get(result.size() - 1);
final String json = new ObjectMapper().writeValueAsString(lastMessage);
return json;
} else {
// メッセージが存在しない場合は、空の JSON オブジェクトを返す
return "{}";
}
}
}
10.2.3.1.3. ツール呼び出しサンプル¶
10.2.3.1.3.1. ツール付きの実装例¶
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import com.fasterxml.jackson.databind.ObjectMapper;
import jp.co.intra_mart.foundation.copilot.action.ActionFactory;
import jp.co.intra_mart.foundation.copilot.action.chat.ChatAction;
import jp.co.intra_mart.foundation.copilot.action.chat.ChatMessage;
import jp.co.intra_mart.foundation.copilot.action.chat.ChatMessage.ChatMessageBuilder;
import jp.co.intra_mart.foundation.copilot.action.chat.ChatOption;
import jp.co.intra_mart.foundation.copilot.action.chat.ToolCall;
import jp.co.intra_mart.foundation.copilot.action.chat.ToolChoice;
import jp.co.intra_mart.foundation.copilot.action.chat.ToolConfig;
import jp.co.intra_mart.foundation.copilot.action.chat.ToolDefinition;
import jp.co.intra_mart.foundation.copilot.tool.ToolJsonHelper;
import jp.co.intra_mart.foundation.copilot.tool.JsonSchemaValidator;
import jp.co.intra_mart.foundation.copilot.tool.SchemaValidationError;
import jp.co.intra_mart.foundation.copilot.tool.annotation.SchemaProperty;
import jp.co.intra_mart.foundation.web_api_maker.annotation.IMAuthentication;
import jp.co.intra_mart.foundation.web_api_maker.annotation.POST;
import jp.co.intra_mart.foundation.web_api_maker.annotation.Parameter;
import jp.co.intra_mart.foundation.web_api_maker.annotation.Path;
import jp.co.intra_mart.foundation.web_api_maker.annotation.Required;
/**
* チャットツール呼び出しのサンプルです。
*
* <pre>
* 使用例:
* POST /sample/chat/tool
* Body:
* {
* "userPrompt": "商品コード「P-001」の在庫数を教えてください。"
* }
* </pre>
*/
@IMAuthentication
public class ChatToolSample {
/**
* 在庫確認ツールのパラメータクラス
*/
public static class InventoryCheckParams {
@StringProperty(description = "商品コード", required = true)
private String productCode;
@StringProperty(description = "倉庫コード(省略時は全倉庫)", required = false)
private String warehouseCode;
/**
* 商品コードを取得します。
* @return 商品コード
*/
public String getProductCode() {
return productCode;
}
/**
* 倉庫コードを取得します。
* @return 倉庫コード(null または空の場合は全倉庫対象)
*/
public String getWarehouseCode() {
return warehouseCode;
}
/**
* 商品コードを設定します。
* @param productCode 商品コード(必須)
*/
public void setProductCode(final String productCode) {
this.productCode = productCode;
}
/**
* 倉庫コードを設定します。
* @param warehouseCode 倉庫コード(省略可能。null または空の場合は全倉庫対象)
*/
public void setWarehouseCode(final String warehouseCode) {
this.warehouseCode = warehouseCode;
}
}
/**
* 在庫確認の実行(模擬実装)
*/
private static String executeInventoryCheck(final ToolCall toolCall) {
try {
// JSONスキーマバリデーションの実行
final ToolDefinition inventoryTool = ToolDefinition.builder().name("check_inventory").description("商品の在庫数を確認します").parametersFromClass(InventoryCheckParams.class).build();
final JsonSchemaValidator validator = new JsonSchemaValidator(inventoryTool.getParameters());
final List<SchemaValidationError> errors = validator.validate(toolCall.getArguments());
if (!errors.isEmpty()) {
// バリデーションエラーがある場合
final StringBuilder errorMessage = new StringBuilder("バリデーションエラー:");
for (final SchemaValidationError error : errors) {
errorMessage.append("\n- ").append(error.getPropertyName()).append(": ").append(error.getMessage());
}
return errorMessage.toString();
}
// ToolJsonHelper を使用して JSON 引数をパース
final InventoryCheckParams params = ToolJsonHelper.deserialize(toolCall.getArguments(), InventoryCheckParams.class);
// 実際の在庫確認処理(ここでは固定値を返す)
final int stockQuantity = 150; // 実際はデータベースなどから取得
return String.format("商品コード %s の在庫数: %d 個", params.getProductCode(), stockQuantity);
} catch (final Exception e) {
return "在庫確認エラー: " + e.getMessage();
}
}
/**
* ユーザから受け取ったプロンプトに対して、チャット応答を生成して返却します。
* @param userPrompt ユーザからのプロンプト
* @return チャット応答
* @throws Exception 応答生成中にエラーが発生した場合
*/
@Path("/sample/chat/tool")
@POST
public String chat(@Required @Parameter(name = "userPrompt") final String userPrompt) throws Exception {
// 1. ChatAction の取得
final ActionFactory factory = ActionFactory.getFactory();
final ChatAction action = factory.getChatAction();
// 2. ツール定義の作成
final ToolDefinition inventoryTool = ToolDefinition.builder().name("check_inventory").description("商品の在庫数を確認します").parametersFromClass(InventoryCheckParams.class).build();
// 3. ツール設定の作成
final ToolConfig toolConfig = new ToolConfig(Arrays.asList(inventoryTool), ToolChoice.builder().auto().build());
// 4. 初回メッセージの作成
final List<ChatMessage> messages = new ArrayList<>();
messages.addAll(ChatMessage.builder().newMessage().withRole("user").addTextContent(userPrompt).build());
// 5. ChatOption の設定
final ChatOption option = new ChatOption();
option.setTemperature(0.0);
// 6. AI の応答を取得(ツール呼び出しを含む)
final List<ChatMessage> response = action.execute(messages, option, toolConfig);
messages.addAll(response);
// 7. ツール呼び出しの処理
final ChatMessage lastMessage = response.get(response.size() - 1);
if (lastMessage.getToolCalls() != null && !lastMessage.getToolCalls().isEmpty()) {
// 新しいメッセージビルダーを作成、ツール結果は "tool" ロールで送信します
final ChatMessageBuilder toolMessageBuilder = ChatMessage.builder().newMessage().withRole("tool");
// 各ツール呼び出しに対して、結果を取得してメッセージに追加
for (final ToolCall toolCall : lastMessage.getToolCalls()) {
System.out.println("ツール呼び出し: " + toolCall.getName());
System.out.println("引数: " + toolCall.getArguments());
// 実際のツール実行(ここでは模擬的な在庫確認)
final String result = executeInventoryCheck(toolCall);
// 実行結果を toolCallId に紐づけて追加
toolMessageBuilder.addToolTextContent(result, toolCall.getId());
}
// 複数のツール実行結果を1つのtoolメッセージにまとめて追加
messages.addAll(toolMessageBuilder.build());
// 8. ツール実行結果を踏まえた最終応答を取得
final List<ChatMessage> finalResponse = action.execute(messages, option, toolConfig);
// 最終応答を出力(最後のアシスタントメッセージのみ出力)
if (finalResponse != null && !finalResponse.isEmpty()) {
// チャットメッセージのリストが存在し、空でない場合は、
// 最後のメッセージを取得して JSON 形式の文字列に変換する
final ChatMessage lastChatMessage = finalResponse.get(finalResponse.size() - 1);
final String json = new ObjectMapper().writeValueAsString(lastChatMessage);
return json;
}
}
// ツール呼び出しが存在しない場合は、空の JSON オブジェクトを返す
return "{}";
}
}
10.2.3.1.3.2. ストリーム形式の実装例¶
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import jp.co.intra_mart.foundation.copilot.action.ActionFactory;
import jp.co.intra_mart.foundation.copilot.action.chat.ChatAction;
import jp.co.intra_mart.foundation.copilot.action.chat.ChatMessage;
import jp.co.intra_mart.foundation.copilot.action.chat.ChatMessage.ChatMessageBuilder;
import jp.co.intra_mart.foundation.copilot.action.chat.ChatOption;
import jp.co.intra_mart.foundation.copilot.action.chat.ToolCall;
import jp.co.intra_mart.foundation.copilot.action.chat.ToolChoice;
import jp.co.intra_mart.foundation.copilot.action.chat.ToolConfig;
import jp.co.intra_mart.foundation.copilot.action.chat.ToolDefinition;
import jp.co.intra_mart.foundation.copilot.exception.CopilotServiceException;
import jp.co.intra_mart.foundation.copilot.tool.ToolJsonHelper;
import jp.co.intra_mart.foundation.copilot.tool.JsonSchemaValidator;
import jp.co.intra_mart.foundation.copilot.tool.SchemaValidationError;
import jp.co.intra_mart.foundation.copilot.tool.annotation.SchemaProperty;
import jp.co.intra_mart.foundation.web_api_maker.annotation.IMAuthentication;
import jp.co.intra_mart.foundation.web_api_maker.annotation.POST;
import jp.co.intra_mart.foundation.web_api_maker.annotation.Parameter;
import jp.co.intra_mart.foundation.web_api_maker.annotation.Path;
import jp.co.intra_mart.foundation.web_api_maker.annotation.PreventWritingResponse;
import jp.co.intra_mart.foundation.web_api_maker.annotation.Required;
/**
* チャットツール呼び出しのストリーミング版サンプルです。
*
* <pre>
* 使用例:
* POST /sample/chat/tool/stream
* Body:
* {
* "userPrompt": "商品コード「P-001」の在庫数を教えてください。"
* }
* </pre>
*/
@IMAuthentication
public class ChatToolStreamingSample {
/**
* 在庫確認ツールのパラメータクラス
*/
public static class InventoryCheckParams {
@StringProperty(description = "商品コード", required = true)
private String productCode;
@StringProperty(description = "倉庫コード(省略時は全倉庫)", required = false)
private String warehouseCode;
/**
* 商品コードを取得します。
* @return 商品コード
*/
public String getProductCode() {
return productCode;
}
/**
* 倉庫コードを取得します。
* @return 倉庫コード(null または空の場合は全倉庫対象)
*/
public String getWarehouseCode() {
return warehouseCode;
}
/**
* 商品コードを設定します。
* @param productCode 商品コード(必須)
*/
public void setProductCode(final String productCode) {
this.productCode = productCode;
}
/**
* 倉庫コードを設定します。
* @param warehouseCode 倉庫コード(省略可能。null または空の場合は全倉庫対象)
*/
public void setWarehouseCode(final String warehouseCode) {
this.warehouseCode = warehouseCode;
}
}
/**
* 在庫確認の実行(模擬実装)
*/
private static String executeInventoryCheck(final ToolCall toolCall) {
try {
// JSONスキーマバリデーションの実行
final ToolDefinition inventoryTool = ToolDefinition.builder().name("check_inventory").description("商品の在庫数を確認します").parametersFromClass(InventoryCheckParams.class).build();
final JsonSchemaValidator validator = new JsonSchemaValidator(inventoryTool.getParameters());
final List<SchemaValidationError> errors = validator.validate(toolCall.getArguments());
if (!errors.isEmpty()) {
// バリデーションエラーがある場合
final StringBuilder errorMessage = new StringBuilder("バリデーションエラー:");
for (final SchemaValidationError error : errors) {
errorMessage.append("\n- ").append(error.getPropertyName()).append(": ").append(error.getMessage());
}
return errorMessage.toString();
}
// ToolJsonHelper を使用して JSON 引数をパース
final InventoryCheckParams params = ToolJsonHelper.deserialize(toolCall.getArguments(), InventoryCheckParams.class);
// 実際の在庫確認処理(ここでは固定値を返す)
final int stockQuantity = 150; // 実際はデータベースなどから取得
return String.format("商品コード %s の在庫数: %d 個", params.getProductCode(), stockQuantity);
} catch (final Exception e) {
return "在庫確認エラー: " + e.getMessage();
}
}
/**
* ユーザから受け取ったプロンプトに対して、チャット応答をリアルタイムにストリーミング形式で返却します。
* @param userPrompt ユーザからのプロンプト
* @param response HTTPレスポンス
* @throws Exception 応答生成中にエラーが発生した場合
*/
@Path("/sample/chat/tool/stream")
@POST
@PreventWritingResponse
public void chatStream(@Required @Parameter(name = "userPrompt") final String userPrompt, final HttpServletResponse response) throws Exception {
// 1. ChatAction の取得
final ActionFactory factory = ActionFactory.getFactory();
final ChatAction action = factory.getChatAction();
// 2. ツール定義の作成
final ToolDefinition inventoryTool = ToolDefinition.builder().name("check_inventory").description("商品の在庫数を確認します").parametersFromClass(InventoryCheckParams.class).build();
// 3. ツール設定の作成
final ToolConfig toolConfig = new ToolConfig(Arrays.asList(inventoryTool), ToolChoice.builder().auto().build());
// 4. 初回メッセージの作成
final List<ChatMessage> messages = new ArrayList<>();
messages.addAll(ChatMessage.builder().newMessage().withRole("user").addTextContent(userPrompt).build());
// 5. ChatOption の設定
final ChatOption option = new ChatOption();
option.setTemperature(0.0);
// 6. レスポンスの設定
response.setContentType("text/event-stream");
response.setCharacterEncoding("UTF-8");
try (final PrintWriter writer = response.getWriter()) {
// 7. 初回の通常実行(ツール呼び出しチェック用)
final List<ChatMessage> firstResponse = action.execute(messages, option, toolConfig);
messages.addAll(firstResponse);
// 8. ツール呼び出しの確認と処理
final ChatMessage lastMessage = firstResponse.get(firstResponse.size() - 1);
if (lastMessage.getToolCalls() != null && !lastMessage.getToolCalls().isEmpty()) {
// 9. ツール呼び出しがある場合の処理
// 新しいメッセージビルダーを作成、ツール結果は "tool" ロールで送信します
final ChatMessageBuilder toolMessageBuilder = ChatMessage.builder().newMessage().withRole("tool");
// 各ツール呼び出しに対して、結果を取得してメッセージに追加
for (final ToolCall toolCall : lastMessage.getToolCalls()) {
System.out.println("ツール呼び出し: " + toolCall.getName());
System.out.println("引数: " + toolCall.getArguments());
// 実際のツール実行(ここでは模擬的な在庫確認)
final String result = executeInventoryCheck(toolCall);
// 実行結果を toolCallId に紐づけて追加
toolMessageBuilder.addToolTextContent(result, toolCall.getId());
}
// 複数のツール実行結果を1つのtoolメッセージにまとめて追加
messages.addAll(toolMessageBuilder.build());
// 10. ツール実行結果を踏まえた最終応答をストリーミングで取得
writer.write("data: {\"type\":\"tool_executed\"}\n\n");
writer.flush();
// ストリーミング実行
action.execute(messages, option, toolConfig, chunk -> {
if (chunk != null && chunk.getDelta() != null && chunk.getDelta().getContent() != null) {
// ストリーミングデータをSSE形式で送信
writer.write("data: " + chunk.getDelta().getContent() + "\n\n");
writer.flush();
}
});
} else {
// 11. ツール呼び出しがない場合は、そのままテキストレスポンスをストリーミング
// 初回応答のコンテンツを取得してストリーミング送信
if (lastMessage.getContent() != null) {
writer.write("data: " + lastMessage.getContent());
writer.flush();
}
}
// 12. ストリーミング終了のマーカー
writer.write("data: [DONE]\n\n");
writer.flush();
} catch (final IOException e) {
throw new CopilotServiceException("ストリーミング中にエラーが発生しました", e);
}
}
}
10.2.3.1.3.3. 完全ストリーム形式の実装例¶
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.servlet.http.HttpServletResponse;
import jp.co.intra_mart.foundation.copilot.action.ActionFactory;
import jp.co.intra_mart.foundation.copilot.action.chat.ChatAction;
import jp.co.intra_mart.foundation.copilot.action.chat.ChatMessage;
import jp.co.intra_mart.foundation.copilot.action.chat.ChatMessage.ChatMessageBuilder;
import jp.co.intra_mart.foundation.copilot.action.chat.ChatOption;
import jp.co.intra_mart.foundation.copilot.action.chat.ToolCall;
import jp.co.intra_mart.foundation.copilot.action.chat.ToolChoice;
import jp.co.intra_mart.foundation.copilot.action.chat.ToolConfig;
import jp.co.intra_mart.foundation.copilot.action.chat.ToolDefinition;
import jp.co.intra_mart.foundation.copilot.exception.CopilotServiceException;
import jp.co.intra_mart.foundation.copilot.tool.ToolJsonHelper;
import jp.co.intra_mart.foundation.copilot.tool.JsonSchemaValidator;
import jp.co.intra_mart.foundation.copilot.tool.SchemaValidationError;
import jp.co.intra_mart.foundation.copilot.tool.annotation.SchemaProperty;
import jp.co.intra_mart.foundation.web_api_maker.annotation.IMAuthentication;
import jp.co.intra_mart.foundation.web_api_maker.annotation.POST;
import jp.co.intra_mart.foundation.web_api_maker.annotation.Parameter;
import jp.co.intra_mart.foundation.web_api_maker.annotation.Path;
import jp.co.intra_mart.foundation.web_api_maker.annotation.PreventWritingResponse;
import jp.co.intra_mart.foundation.web_api_maker.annotation.Required;
/**
* チャットツール呼び出しの完全ストリーミング版サンプルです。 ツール呼び出しも含めて全てストリーミングで処理します。
*
* <pre>
* 使用例:
* POST /sample/chat/tool/stream/dual
* Body:
* {
* "userPrompt": "商品コード「P-001」の在庫数を教えてください。"
* }
* </pre>
*/
@IMAuthentication
public class ChatToolDualStreamingSample {
/**
* 在庫確認ツールのパラメータクラス
*/
public static class InventoryCheckParams {
@StringProperty(description = "商品コード", required = true)
private String productCode;
@StringProperty(description = "倉庫コード(省略時は全倉庫)", required = false)
private String warehouseCode;
/**
* 商品コードを取得します。
* @return 商品コード
*/
public String getProductCode() {
return productCode;
}
/**
* 倉庫コードを取得します。
* @return 倉庫コード(null または空の場合は全倉庫対象)
*/
public String getWarehouseCode() {
return warehouseCode;
}
/**
* 商品コードを設定します。
* @param productCode 商品コード(必須)
*/
public void setProductCode(final String productCode) {
this.productCode = productCode;
}
/**
* 倉庫コードを設定します。
* @param warehouseCode 倉庫コード(省略可能。null または空の場合は全倉庫対象)
*/
public void setWarehouseCode(final String warehouseCode) {
this.warehouseCode = warehouseCode;
}
}
/**
* ストリーミング処理の状態を保持するクラス
*/
private static class StreamingState {
private final List<ToolCall> collectedToolCalls = new ArrayList<>();
private final StringBuilder currentContent = new StringBuilder();
private final AtomicBoolean hasToolCalls = new AtomicBoolean(false);
private String currentToolCallId = null;
private String currentToolName = null;
private StringBuilder currentArguments = new StringBuilder();
}
/**
* 在庫確認の実行(模擬実装)
*/
private static String executeInventoryCheck(final ToolCall toolCall) {
try {
// JSONスキーマバリデーションの実行
final ToolDefinition inventoryTool = ToolDefinition.builder().name("check_inventory").description("商品の在庫数を確認します").parametersFromClass(InventoryCheckParams.class).build();
final JsonSchemaValidator validator = new JsonSchemaValidator(inventoryTool.getParameters());
final List<SchemaValidationError> errors = validator.validate(toolCall.getArguments());
if (!errors.isEmpty()) {
// バリデーションエラーがある場合
final StringBuilder errorMessage = new StringBuilder("バリデーションエラー:");
for (final SchemaValidationError error : errors) {
errorMessage.append("\n- ").append(error.getPropertyName()).append(": ").append(error.getMessage());
}
return errorMessage.toString();
}
// ToolJsonHelper を使用して JSON 引数をパース
final InventoryCheckParams params = ToolJsonHelper.deserialize(toolCall.getArguments(), InventoryCheckParams.class);
// 実際の在庫確認処理(ここでは固定値を返す)
final int stockQuantity = 150; // 実際はデータベースなどから取得
return String.format("商品コード %s の在庫数: %d 個", params.getProductCode(), stockQuantity);
} catch (final Exception e) {
return "在庫確認エラー: " + e.getMessage();
}
}
/**
* ユーザから受け取ったプロンプトに対して、チャット応答を完全ストリーミング形式で返却します。
* @param userPrompt ユーザからのプロンプト
* @param response HTTPレスポンス
* @throws Exception 応答生成中にエラーが発生した場合
*/
@Path("/sample/chat/tool/stream/dual")
@POST
@PreventWritingResponse
public void chatStream(@Required @Parameter(name = "userPrompt") final String userPrompt, final HttpServletResponse response) throws Exception {
// 1. ChatAction の取得
final ActionFactory factory = ActionFactory.getFactory();
final ChatAction action = factory.getChatAction();
// 2. ツール定義の作成
final ToolDefinition inventoryTool = ToolDefinition.builder().name("check_inventory").description("商品の在庫数を確認します").parametersFromClass(InventoryCheckParams.class).build();
// 3. ツール設定の作成
final ToolConfig toolConfig = new ToolConfig(Arrays.asList(inventoryTool), ToolChoice.builder().auto().build());
// 4. 初回メッセージの作成
final List<ChatMessage> messages = new ArrayList<>();
messages.addAll(ChatMessage.builder().newMessage().withRole("user").addTextContent(userPrompt).build());
// 5. ChatOption の設定
final ChatOption option = new ChatOption();
option.setTemperature(0.0);
// 6. レスポンスの設定
response.setContentType("text/event-stream");
response.setCharacterEncoding("UTF-8");
try (final PrintWriter writer = response.getWriter()) {
// 7. ストリーミング開始通知
writer.write("data: {\"type\":\"stream_start\"}\n\n");
writer.flush();
// 8. 再帰的なストリーミング実行
executeStreamingWithTools(messages, action, option, toolConfig, writer);
// 9. ストリーミング終了のマーカー
writer.write("data: [DONE]\n\n");
writer.flush();
} catch (final IOException e) {
throw new CopilotServiceException("ストリーミング中にエラーが発生しました", e);
}
}
/**
* 再帰的にストリーミング処理を実行
* @throws Exception
*/
private void executeStreamingWithTools(final List<ChatMessage> messages, final ChatAction action, final ChatOption option, final ToolConfig toolConfig, final PrintWriter writer) throws Exception {
final StreamingState state = new StreamingState();
// ストリーミング実行
action.execute(messages, option, toolConfig, chunk -> {
if (chunk == null || chunk.getDelta() == null) {
return;
}
// テキストコンテンツの処理
if (chunk.getDelta().getContent() != null) {
state.currentContent.append(chunk.getDelta().getContent());
writer.write("data: " + chunk.getDelta().getContent() + "\n\n");
writer.flush();
}
// ツール呼び出しの処理
if (chunk.getDelta().getToolCalls() != null && !chunk.getDelta().getToolCalls().isEmpty()) {
state.hasToolCalls.set(true);
for (final ToolCall toolCall : chunk.getDelta().getToolCalls()) {
// 新しいツール呼び出しの開始
if (toolCall.getId() != null) {
// 前のツール呼び出しがあれば完成させる
if (state.currentToolCallId != null) {
final ToolCall completedCall = ToolCall.builder().id(state.currentToolCallId).name(state.currentToolName).arguments(state.currentArguments.toString()).build();
state.collectedToolCalls.add(completedCall);
}
// 新しいツール呼び出しの初期化
state.currentToolCallId = toolCall.getId();
state.currentToolName = toolCall.getName();
state.currentArguments = new StringBuilder();
}
// 引数の収集
if (toolCall.getArguments() != null && !toolCall.getArguments().isEmpty()) {
state.currentArguments.append(toolCall.getArguments());
}
}
}
});
// 最後のツール呼び出しを完成させる
if (state.currentToolCallId != null) {
final ToolCall completedCall = ToolCall.builder().id(state.currentToolCallId).name(state.currentToolName).arguments(state.currentArguments.toString()).build();
state.collectedToolCalls.add(completedCall);
}
// ツール呼び出しがある場合は処理して再帰実行
if (state.hasToolCalls.get() && !state.collectedToolCalls.isEmpty()) {
// ツール実行通知
writer.write("data: {\"type\":\"tool_execution_start\"}\n\n");
writer.flush();
// アシスタントメッセージ(ツール呼び出し付き)を追加
if (state.currentContent.length() > 0) {
messages.addAll(ChatMessage.builder().newMessage().withRole("assistant").addTextContent(state.currentContent.toString()).toolCalls(state.collectedToolCalls).build());
} else {
messages.addAll(ChatMessage.builder().newMessage().withRole("assistant").toolCalls(state.collectedToolCalls).build());
}
// ツール実行
final ChatMessageBuilder toolMessageBuilder = ChatMessage.builder().newMessage().withRole("tool");
for (final ToolCall toolCall : state.collectedToolCalls) {
System.out.println("ツール呼び出し: " + toolCall.getName());
System.out.println("引数: " + toolCall.getArguments());
// 実際のツール実行
final String result = executeInventoryCheck(toolCall);
// 実行結果を toolCallId に紐づけて追加
toolMessageBuilder.addToolTextContent(result, toolCall.getId());
// ツール実行結果を通知
writer.write("data: {\"type\":\"tool_executed\",\"tool\":\"" + toolCall.getName() + "\",\"result\":\"" + result.replace("\"", "\\\"") + "\"}\n\n");
writer.flush();
}
// ツール実行結果をメッセージに追加
messages.addAll(toolMessageBuilder.build());
// 再帰的にストリーミング実行(ツール実行後の応答生成)
writer.write("data: {\"type\":\"tool_response_start\"}\n\n");
writer.flush();
executeStreamingWithTools(messages, action, option, toolConfig, writer);
}
}
}