diff --git a/.github/workflows/dotnet-desktop.yml b/.github/workflows/dotnet-desktop.yml index c5030e8..d082363 100644 --- a/.github/workflows/dotnet-desktop.yml +++ b/.github/workflows/dotnet-desktop.yml @@ -1,78 +1,42 @@ name: Build WPF Application on: - push: - branches: [ "main" ] pull_request: - branches: [ "main" ] + branches: + - main + - release/** -permissions: - contents: write +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true jobs: build: - runs-on: windows-latest - + runs-on: windows-2025-vs2026 + steps: - - uses: actions/checkout@v4 - - - name: Setup MSBuild - uses: microsoft/setup-msbuild@v1 - with: - vs-version: '[17.0, 18.0]' - - - name: Setup NuGet - uses: NuGet/setup-nuget@v1 - with: - nuget-version: 'latest' + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - name: Setup MSBuild + uses: microsoft/setup-msbuild@v2 + with: + vs-version: '17.0' - - name: Restore NuGet packages - run: nuget restore SampleCSharpUI.sln - - - name: Build WPF Application - run: msbuild SampleCSharpUI.sln /p:Configuration=Release /p:Platform="Any CPU" - - - name: Upload Build Artifacts - uses: actions/upload-artifact@v4 - with: - name: SampleCSharpUI-${{ github.run_number }} - path: | - $(Build.ArtifactStagingDirectory)\** - **\bin\Release\** - retention-days: 7 - - - name: Prepare release ZIP - shell: pwsh - run: | - $zipName = "SampleCSharpUI-${{ github.run_number }}.zip" - # ビルド出力 (任意のサブフォルダーの bin\Release を含むすべてのファイル) を収集 - $files = Get-ChildItem -Path . -Recurse -File | Where-Object { $_.FullName -like "*\bin\Release\*" } | Select-Object -ExpandProperty FullName - if (-not $files) { - Write-Error "No files found under bin\\Release" - exit 1 - } - Compress-Archive -Path $files -DestinationPath $zipName -Force - Write-Output "Created $zipName" + # ★ 追加:packages.config 用 + - name: NuGet Restore + run: nuget restore SampleCSharpUI.sln - - name: Create GitHub Release - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - id: create_release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: v${{ github.run_number }} - release_name: Release ${{ github.run_number }} - draft: false - prerelease: false + # 念のため残してもOK(SDK-style project 用) + - name: dotnet Restore + run: dotnet restore SampleCSharpUI.sln - - name: Upload ZIP to Release - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: SampleCSharpUI-${{ github.run_number }}.zip - asset_name: SampleCSharpUI-${{ github.run_number }}.zip - asset_content_type: application/zipp + - name: Build (Release) + run: msbuild SampleCSharpUI.sln ` + /p:Configuration=Release ` + /p:Platform="Any CPU" ` + /t:Build diff --git a/.github/workflows/release-upload-zip.yml b/.github/workflows/release-upload-zip.yml new file mode 100644 index 0000000..ded636c --- /dev/null +++ b/.github/workflows/release-upload-zip.yml @@ -0,0 +1,67 @@ +name: Release - Upload ZIP + +on: + push: + tags: + - 'v*' # v1.3.0.0 など + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + release: + runs-on: windows-2025-vs2026 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - name: Setup MSBuild + uses: microsoft/setup-msbuild@v2 + with: + vs-version: '17.0' + + # packages.config 対応 + - name: NuGet Restore + run: nuget restore SampleCSharpUI.sln + + - name: Build (Release) + run: msbuild SampleCSharpUI.sln ` + /p:Configuration=Release ` + /p:Platform="Any CPU" ` + /t:Build + + # ZIP 作成(タグ名を使用) + - name: Create ZIP + run: | + Compress-Archive ` + -Path SampleCSharpUI/bin/Release/* ` + -DestinationPath SampleCSharpUI-${{ github.ref_name }}.zip + + # Release 作成(タグ名を使用) + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref_name }} + release_name: Release ${{ github.ref_name }} + draft: false + prerelease: false + + # ZIP を Release に Upload(ご指定どおり) + - name: Upload ZIP to Release + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: SampleCSharpUI-${{ github.ref_name }}.zip + asset_name: SampleCSharpUI-${{ github.ref_name }}.zip + asset_content_type: application/zip diff --git a/README.md b/README.md index 09244a5..c1dc15c 100644 --- a/README.md +++ b/README.md @@ -7,14 +7,16 @@ Fujitsu Cloud Service Genearative AI Platform の API を利用するための C ## 起動 1. SampleCSharpUI.exe をクリックして起動 2. 【初回起動時のみ】 -自動的にテナント名とクライアントIDを入力する設定画面を表示するので、利用している Generative AI Platform 環境のテナント名とクライアント ID を入力して [OK] をクリックして保存してください。 +自動的にテナント名とクライアントIDを入力する [ 設定 ] 画面を表示するので、利用している Generative AI Platform 環境のテナント名とクライアント ID を入力して [OK] をクリックして保存してください。 3. 自動的にサインインが始まりますので、利用している EntraID の ID とパスワードを入力してください。 4. 「 General Use 」というルームがない場合は、自動的にRAGなしでルームを作成します。 5. 前回利用時のルームの内容を自動的に表示します。初回起動時のみ「 General Use 」ルームが選択されます。 - 「 General Use 」ルームは起動時に会話内容が全クリアされる特別なルームです。 - 他のルームは再起動しても会話内容は維持されます。 -### 設定画面詳細 +### [ 設定 ] 画面詳細 +[ ファイル ]-[ 接続設定 ]メニューで接続情報を後から変更可能です。 + | 項目 | 用途 | | ---- | ---- | | 対話認証 | EntraID でサインインする場合に選択 | @@ -106,27 +108,34 @@ RAG自体を削除したり、RAGに格納されているデータを変更し - 過去履歴を踏まえたプロンプトパターンの実現 - ルームへ会話を記録することによる後日閲覧の実現 - 各種トークン数の設定 -しかしながら、ルーム不要でTakaneに対してプロンプトを入力し回答を得るだけでよい場合には、ルームの設定や管理などの事前実行が必要なWebAPI呼び出しは煩雑に感じてしまいます。 +しかしながら、ルーム不要でTakaneに対してプロンプトを入力し回答を得るだけでよい場合には、ルームの設定や管理などの事前実行が必要なWebAPI呼び出しは煩雑に感じてしまいます。 そのようなユースケースでは「ルームなし会話」モードがあります。 ### 本サンプル画面での使い方 -ルームの選択で「(none)」を選択するとチャットルームなしでの利用となります。 +ルームの選択で「(none)」を選択するとチャットルームなしでの利用となります。 サンプルコード内で画面に表示されている過去履歴を渡しているので、過去履歴を踏まえたやりとりが可能となっておりますが、過去履歴を渡さずにAPIコールをすれば、必ず入力したプロンプトだけを踏まえた回答となります。 #### 制限事項 -ルームを「(none)」に選択してる場合は、ルーム編集や新規作成の機能はご利用になれません。 +ルームを「(none)」に選択してる場合は、ルーム編集や新規作成の機能はご利用になれません。 ルーム編集やルーム新規作成が必要な場合は、他のルームを選択してから実行してください。 -### ルームなし会話の実現方法 -具体的には、BODYに入力されたプロンプトを設定し、下記のAPIをPOSTすることで戻り値としてAIの回答を得ることが出来ます。 +### マルチモーダル対応 +マルチモーダル対応 + +ルームなし会話では、マルチモーダル対応として、添付画像に対する質問を入力することができます。 +添付できる画像ファイル形式は、png、jpeg、webp、非アニメーションgif形式の5MB以下となります。 +1. 画像の添付方法は、入力欄下の [+] ボタンをクリックしてファイル選択 +2. 画像の添付解除は、画像横の [-] ボタンをクリックして解除 + +マルチモーダル対応は、Cohere v2 Chat 互換 API (/v2/chat)、または、Cohere OpenAI Chat Completions 互換 API (/compatibility/v1/chat/completions) を呼び出すことで実現可能です。 +サンプルコードでは、Cohere v2 Chat 互換 API を使用しています。 + -``` -/api/v1/action/defined/text:simple_chat/call -``` +OpenAI 互換 API、または、Cohere V2 Chat 互換 API を呼び出すことで実現可能です。 # サンプルコードの build 方法 -サンプルコードは、Visual Studio 2026 または、Visual Studio Code を使用して実行ファイルを build できます。 -Visual Studio 2026 であれば、SampleCSharpUI.sln を開いていただければ、あとは UI 上で実行や build が可能です。 +サンプルコードは、Visual Studio 2026 または、Visual Studio Code を使用して実行ファイルを build できます。 +Visual Studio 2026 であれば、SampleCSharpUI.sln を開いていただければ、あとは UI 上で実行や build が可能です。 Visual Studio Code の場合は、環境設定などが必要です。 # 最後に diff --git a/SampleCSharpUI/Commons/APIData.cs b/SampleCSharpUI/Commons/APIData.cs index d7f163b..ede1bf3 100644 --- a/SampleCSharpUI/Commons/APIData.cs +++ b/SampleCSharpUI/Commons/APIData.cs @@ -208,5 +208,58 @@ public class TChatStream [DataMember] public string token { get; set; } } + + [DataContract] + public class TCohereV2ChatRequest + { + [DataMember] + public string model { get; set; } + [DataMember] + public TCohereV2ChatMessage[] messages { get; set; } + [DataMember] + public float temperature { get; set; } + [DataMember] + public uint max_tokens { get; set; } + + } + + [DataContract] + public class TCohereV2ChatMessage + { + [DataMember] + public string role { get; set; } + [DataMember] + public TContent[] content { get; set; } + } + + [DataContract] + public class TContent + { + [DataMember] + public string type { get; set; } + [DataMember] + public string text { get; set; } + [DataMember] + public TImageUrl image_url { get; set; } + } + + [DataContract] + public class TImageUrl + { + [DataMember] + public string url { get; set; } + } + + [DataContract] + public class TCohereV2ChatResponse + { + [DataMember] + public string id { get; set; } + [DataMember] + public TCohereV2ChatMessage message { get; set; } + [DataMember] + public string finish_reason { get; set; } + + } } } diff --git a/SampleCSharpUI/Commons/Base64Helper.cs b/SampleCSharpUI/Commons/Base64Helper.cs new file mode 100644 index 0000000..b86850e --- /dev/null +++ b/SampleCSharpUI/Commons/Base64Helper.cs @@ -0,0 +1,70 @@ +using System; +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; + +namespace SampleCSharpUI.Commons +{ + /// + /// 画像データをBase64エンコードするためのヘルパーメソッドを提供します。 + /// + internal static class Base64Helper + { + /// + /// 指定したファイルパスの画像ファイルを非同期に読み込み、Base64エンコードした文字列を返します。 + /// + /// 読み込む画像ファイルのパス。null、空白は許可されません。 + /// 画像データをBase64エンコードした文字列を表す非同期タスク。 + /// が null または空白の場合にスローされます。 + /// 指定したパスのファイルが存在しない場合にスローされる可能性があります。 + /// ファイルアクセス権が不足している場合にスローされる可能性があります。 + /// ファイル サイズが int.MaxValue を超える場合にスローされます(メモリ処理不可)。 + /// + /// このメソッドはファイル全体をメモリに読み込んでからBase64変換を行います。 + /// 大きなファイルを扱う場合はメモリ使用量に注意してください。 + /// ファイルの部分読み込みを考慮した安全な読み取りループを使用しています。 + /// + internal static async Task ImageFileToBase64Async(string path) + { + if (string.IsNullOrWhiteSpace(path)) + throw new ArgumentException("path is null or empty", nameof(path)); + + using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, useAsync: true)) + { + if (fs.Length > int.MaxValue) + throw new NotSupportedException("File too large to process in memory."); + + var bytes = new byte[checked((int)fs.Length)]; + var read = 0; + while (read < bytes.Length) + { + var r = await fs.ReadAsync(bytes, read, bytes.Length - read).ConfigureAwait(false); + if (r == 0) break; + read += r; + } + + return Convert.ToBase64String(bytes); + } + } + + /// + /// 指定した URL からバイト配列を取得し、Base64 エンコードした文字列を返します。 + /// + /// 画像などのリソースを取得する URL。 + /// 取得したデータをBase64エンコードした文字列を表す非同期タスク。 + /// が null の場合にスローされる可能性があります。 + /// HTTP 要求の送信や応答の取得に失敗した場合にスローされます。 + /// + /// この実装では呼び出しごとに新しい を生成しています。 + /// 長時間または高頻度で使用する場合は、ソケット枯渇を避けるために を再利用することを検討してください。 + /// + internal static async Task ImageUrlToBase64Async(string url) + { + using (var http = new HttpClient()) + { + var bytes = await http.GetByteArrayAsync(url).ConfigureAwait(false); + return Convert.ToBase64String(bytes); + } + } + } +} diff --git a/SampleCSharpUI/Commons/HttpHelper.cs b/SampleCSharpUI/Commons/HttpHelper.cs index 315dc28..48d688d 100644 --- a/SampleCSharpUI/Commons/HttpHelper.cs +++ b/SampleCSharpUI/Commons/HttpHelper.cs @@ -147,8 +147,16 @@ private static async Task SendRequestAsync(HttpMethod method, string api // リクエスト送信とレスポンス受信 using (var response = await SendClient.SendAsync(request)) { - response.EnsureSuccessStatusCode(); - answer = await response.Content.ReadAsStringAsync(); + if (response.StatusCode == System.Net.HttpStatusCode.BadRequest) + { + answer = await response.Content.ReadAsStringAsync(); + + } + else + { + response.EnsureSuccessStatusCode(); + answer = await response.Content.ReadAsStringAsync(); + } } } } diff --git a/SampleCSharpUI/Converters/Null2CollapsedConverter.cs b/SampleCSharpUI/Converters/Null2CollapsedConverter.cs new file mode 100644 index 0000000..559a3db --- /dev/null +++ b/SampleCSharpUI/Converters/Null2CollapsedConverter.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Data; + +namespace SampleCSharpUI.Converters +{ + internal class Null2CollapsedConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is string) + { + return string.IsNullOrEmpty((string)value) ? Visibility.Collapsed : Visibility.Visible; + } + else + { + return Visibility.Collapsed; + } + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/SampleCSharpUI/Models/ChatModel.cs b/SampleCSharpUI/Models/ChatModel.cs index b01785d..b5e4aee 100644 --- a/SampleCSharpUI/Models/ChatModel.cs +++ b/SampleCSharpUI/Models/ChatModel.cs @@ -8,6 +8,7 @@ using System.IO; using System.Linq; using System.Runtime.CompilerServices; +using System.Security.Policy; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -175,8 +176,8 @@ public ChatModel() } #region "チャット関連" - public ObservableCollection ChatRooms = new ObservableCollection(); - public ObservableCollection Messages = new ObservableCollection(); + public ObservableCollection ChatRooms { get; set; } = new ObservableCollection(); + public ObservableCollection Messages { get; set; } = new ObservableCollection(); private Models.TDataChatRoom _SelectedChatRoom = null; public Models.TDataChatRoom SelectedChatRoom @@ -226,15 +227,16 @@ internal async Task GetChatRoomsAsync(bool isUseNone = false) { var ser = new System.Runtime.Serialization.Json.DataContractJsonSerializer(typeof(List)); { - var result = ser.ReadObject(json) as List; - foreach (var chat in result) + var results = ser.ReadObject(json) as List; + foreach (var item in results?.OrderByDescending((x) => x.created_date)) { var chatRoom = new Models.TDataChatRoom() { - ID = chat.id, - Name = chat.name, - ChatTemplateId = chat.chat_template_id, - RetrieverIDs = chat.retriever_ids, + ID = item.id, + Name = item.name, + ChatTemplateId = item.chat_template_id, + RetrieverIDs = item.retriever_ids, + CreateDateTime = DateTimeOffset.FromUnixTimeMilliseconds(item.created_date).ToLocalTime().DateTime, }; this.ChatRooms.Add(chatRoom); } @@ -455,10 +457,17 @@ internal async Task SendMessageAsync(string id, string inputText) { // 質問のメッセージ追加 var item = ser.ReadObject(json) as APIData.TChatMessage; - this.SetMessage(item.role, - item.content, - DateTimeOffset.FromUnixTimeMilliseconds(item.timeunix).ToLocalTime().DateTime, - item.ref_chunks is null ? new List() : item.ref_chunks?.Select((x) => x.text.Replace("\n\n", "\n")).ToList()); + if (item?.role != null) + { + this.SetMessage(item.role, + item.content, + DateTimeOffset.FromUnixTimeMilliseconds(item.timeunix).ToLocalTime().DateTime, + item.ref_chunks is null ? new List() : item.ref_chunks?.Select((x) => x.text.Replace("\n\n", "\n")).ToList()); + } + else + { + throw new Exception(jsonString); + } } json.Close(); @@ -531,10 +540,17 @@ internal async Task SendMessageStreamingAsync(string id, string inputText) { // 質問のメッセージ追加 var item = ser.ReadObject(json) as APIData.TChatMessage; - this.SetMessage(item.role, - item.content, - DateTimeOffset.FromUnixTimeMilliseconds(item.timeunix).ToLocalTime().DateTime, - item.ref_chunks is null ? new List() : item.ref_chunks?.Select((x) => x.text.Replace("\n\n", "\n")).ToList()); + if (item?.role != null) + { + this.SetMessage(item.role, + item.content, + DateTimeOffset.FromUnixTimeMilliseconds(item.timeunix).ToLocalTime().DateTime, + item.ref_chunks is null ? new List() : item.ref_chunks?.Select((x) => x.text.Replace("\n\n", "\n")).ToList()); + } + else + { + throw new Exception(jsonString); + } } json.Close(); @@ -610,7 +626,8 @@ internal async Task SendMessageStreamingAsync(string id, string inputText) } OnPropertyChanged("Messages_Item"); } - catch { + catch + { this.IsStreaming = false; // エラー発生時はAI回答欄を削除する。 this.Messages.Remove(this.Messages.Where((x) => x.Id == msgId).FirstOrDefault()); @@ -645,10 +662,11 @@ private async Task GetLastAIResponseAsync(string id) var result = ser.ReadObject(json) as APIData.TChat; var item = result.messages.Where((x) => x.role == "ai").LastOrDefault(); var lastItem = this.Messages.Where((x) => x.Role == "ai").LastOrDefault(); - if (lastItem != null) { + if (lastItem != null && !string.IsNullOrEmpty(lastItem.Content)) + { SetMessage(lastItem.Id, - item.role, item.content, - DateTimeOffset.FromUnixTimeMilliseconds(item.timeunix).ToLocalTime().DateTime, + item.role, item.content, + DateTimeOffset.FromUnixTimeMilliseconds(item.timeunix).ToLocalTime().DateTime, item.ref_chunks is null ? new List() : item.ref_chunks?.Select((x) => x.text.Replace("\n\n", "\n")).ToList()); } @@ -698,7 +716,14 @@ internal async Task SendMessageAsync(List histories, float temperature { // 回答のメッセージ追加 var item = ser.ReadObject(json) as APIData.TNonRoomResponse; - this.SetMessage(msgId, "ai", item.answer, DateTime.UtcNow.ToLocalTime(), new List()); + if (item?.answer != null) + { + this.SetMessage(msgId, "ai", item.answer, DateTime.UtcNow.ToLocalTime(), new List()); + } + else + { + throw new Exception(jsonString); + } } json.Close(); @@ -742,15 +767,39 @@ private APIData.TChatMessage[] SetHistories(List histories) } } + // 履歴設定(チャットルームなし) + private APIData.TCohereV2ChatMessage[] SetCohereV2ChatHistories(List histories) + { + if (histories == null || histories.Count == 0) + { + return new APIData.TCohereV2ChatMessage[] { }; + } + else + { + var messages = new APIData.TCohereV2ChatMessage[histories.Count]; + for (int i = 0; i < histories.Count; i++) + { + var history = histories[i]; + messages[i] = new APIData.TCohereV2ChatMessage() + { + role = history.Role == "ai" ? "assistant" : "user", + content = new APIData.TContent[] { new APIData.TContent() { type = "text", text = history.Content } }, + }; + } + return messages; + } + } + // メッセージ追加(チャットルームなし) - private Guid SetMessage(string role, string content, DateTime time, List refs) + private Guid SetMessage(string role, string content, DateTime time, List refs, string image = null) { var msg = new TMessage() { Role = role, Content = content, Time = time, - Refs = refs + Refs = refs, + Image = image }; msg.PropertyChanged += (s, e) => { OnPropertyChanged("Messages_Item"); }; this.Messages.Add(msg); @@ -778,6 +827,103 @@ private Guid SetMessage(Guid id, string role, string content, DateTime time, Lis } } + + /// + /// プロンプト入力(チャットルームなし/マルチモーダル) + /// + /// 入力 + internal async Task SendMessageWithFileAsync(List histories, float temperature, int token, string inputText, string filePath) + { + var content = inputText ?? string.Empty; + var base64ImageData = !string.IsNullOrEmpty(filePath) ? await Base64Helper.ImageFileToBase64Async(filePath) : string.Empty; + if (!string.IsNullOrWhiteSpace(content)) + { + var body = new APIData.TCohereV2ChatRequest() + { + model = "takane", + messages = this.SetCohereV2ChatHistories(histories), + temperature = temperature, + max_tokens = (uint)token, + }; + + // ここで配列の末尾に要素を追加する(body.messages が配列であることを前提) + var existing = body.messages ?? new APIData.TCohereV2ChatMessage[] { }; + var newArr = new APIData.TCohereV2ChatMessage[existing.Length + 1]; + if (existing.Length > 0) + { + Array.Copy(existing, newArr, existing.Length); + } + if (!string.IsNullOrEmpty(base64ImageData)) + { + newArr[newArr.Length - 1] = new APIData.TCohereV2ChatMessage() + { + role = "user", + content = new APIData.TContent[] { + new APIData.TContent() { type = "text", text = inputText }, + new APIData.TContent() { type = "image_url", image_url = new APIData.TImageUrl() { url= $"data:image/png;base64,{base64ImageData}" } } + }, + }; + } + else + { + newArr[newArr.Length - 1] = new APIData.TCohereV2ChatMessage() + { + role = "user", + content = new APIData.TContent[] { + new APIData.TContent() { type = "text", text = inputText }, + }, + }; + } + body.messages = newArr; + + // 質問のメッセージ追加 + this.SetMessage("user", content, DateTime.UtcNow.ToLocalTime(), new List(), filePath); + + // ここで body を JSON 文字列にシリアライズして変数に格納する + using (var ms = new MemoryStream()) + { + var msgId = this.SetMessage("ai", Resources.Streaming, DateTime.UtcNow.ToLocalTime(), new List()); + OnPropertyChanged("Messages_Item"); + var serializer = new System.Runtime.Serialization.Json.DataContractJsonSerializer(typeof(APIData.TCohereV2ChatRequest)); + { + serializer.WriteObject(ms, body); + try + { + var bodyJsonString = Encoding.UTF8.GetString(ms.ToArray()); + var jsonString = await HttpHelper.PostRequestAsync($"/api/v1/pass-through/takane/v2/chat", this.IdToken, bodyJsonString); + using (var json = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(jsonString))) + { + var ser = new System.Runtime.Serialization.Json.DataContractJsonSerializer(typeof(APIData.TCohereV2ChatResponse)); + { + // 回答のメッセージ追加 + var item = ser.ReadObject(json) as APIData.TCohereV2ChatResponse; + if (item?.id != null) + { + this.SetMessage(msgId, "ai", item.message.content[0].text, DateTime.UtcNow.ToLocalTime(), new List()); + } + else + { + throw new Exception(jsonString); + } + } + json.Close(); + + // 最新行表示 + OnPropertyChanged("Messages_Item"); + } + } + catch (Exception ex) + { + // エラー発生時はAI回答欄を削除し、exceptionを投げる + this.Messages.Remove(this.Messages.Where((x) => x.Id == msgId).FirstOrDefault()); + throw ex; + } + } + } + } + } + + /// /// 最新の入力プロンプトまでを削除する(AIからの回答がある場合は、その回答まで削除する /// @@ -878,6 +1024,7 @@ public class TDataChatRoom public string Name { get; set; } = string.Empty; public string ChatTemplateId { get; set; } = string.Empty; public string[] RetrieverIDs { get; set; } = null; + public DateTime CreateDateTime { get; set; } = DateTime.Now; } /// @@ -889,5 +1036,6 @@ public class TDataRetriever public string Name { get; set; } = string.Empty; public string EmbeddingModel { get; set; } = string.Empty; public string[] OriginIDs { get; set; } = null; + public DateTime CreateDateTime { get; set; } = DateTime.Now; } } \ No newline at end of file diff --git a/SampleCSharpUI/Models/RetrieverModel.cs b/SampleCSharpUI/Models/RetrieverModel.cs index a78013e..6e72d6f 100644 --- a/SampleCSharpUI/Models/RetrieverModel.cs +++ b/SampleCSharpUI/Models/RetrieverModel.cs @@ -44,13 +44,13 @@ public Models.TDataRetriever SelectedRetriever /// /// 対象リトリーバ /// - private APIData.TRetriever _Retriever = null; + private APIData.TRetriever _RetrieverData = null; public APIData.TRetriever RetrieverData { - get { return this._Retriever; } + get { return this._RetrieverData; } set { - this._Retriever = value; + this._RetrieverData = value; OnPropertyChanged(); } } @@ -104,12 +104,13 @@ internal async Task GetRetrieversAsync(bool isUseNone = false) var ser = new System.Runtime.Serialization.Json.DataContractJsonSerializer(typeof(APIData.TRetrievers)); { var result = ser.ReadObject(json) as APIData.TRetrievers; - foreach (var item in result.results) + foreach (var item in result.results?.OrderByDescending((x)=> x.created_at)) { var retriever = new Models.TDataRetriever() { ID = item.id, - Name = item.name + Name = item.name, + CreateDateTime = DateTimeOffset.FromUnixTimeMilliseconds(item.created_at).ToLocalTime().DateTime, }; this.Retrievers.Add(retriever); } diff --git a/SampleCSharpUI/Models/TMessage.cs b/SampleCSharpUI/Models/TMessage.cs index eda7ba7..9d689f2 100644 --- a/SampleCSharpUI/Models/TMessage.cs +++ b/SampleCSharpUI/Models/TMessage.cs @@ -76,6 +76,23 @@ internal set } } + /// + /// 添付イメージファイル名 + /// + private string _Image { get; set; } = string.Empty; + public string Image + { + get { return _Image; } + internal set + { + if (_Image != value) + { + _Image = value; + OnPropertyChanged(); + } + } + } + // プロパティが変更されたときに通知するイベント public event PropertyChangedEventHandler PropertyChanged; diff --git a/SampleCSharpUI/Properties/AssemblyInfo.cs b/SampleCSharpUI/Properties/AssemblyInfo.cs index f3fd2b3..7949290 100644 --- a/SampleCSharpUI/Properties/AssemblyInfo.cs +++ b/SampleCSharpUI/Properties/AssemblyInfo.cs @@ -48,5 +48,5 @@ // ビルド番号 // リビジョン // -[assembly: AssemblyVersion("1.2.0.0")] -[assembly: AssemblyFileVersion("1.2.0.0")] +[assembly: AssemblyVersion("1.3.0.0")] +[assembly: AssemblyFileVersion("1.3.0.0")] diff --git a/SampleCSharpUI/Properties/Resources.Designer.cs b/SampleCSharpUI/Properties/Resources.Designer.cs index c59bd1f..0ca83d3 100644 --- a/SampleCSharpUI/Properties/Resources.Designer.cs +++ b/SampleCSharpUI/Properties/Resources.Designer.cs @@ -78,6 +78,33 @@ public static string AnswerTokenBudget { } } + /// + /// + に類似しているローカライズされた文字列を検索します。 + /// + public static string Attach { + get { + return ResourceManager.GetString("Attach", resourceCulture); + } + } + + /// + /// ファイルを添付 に類似しているローカライズされた文字列を検索します。 + /// + public static string AttachFile { + get { + return ResourceManager.GetString("AttachFile", resourceCulture); + } + } + + /// + /// 画像ファイル (*.png;*.jpeg;*.jpg;*.gif;*.webp)|*.png;*.jpeg;*.jpg;*.gif;*.webp に類似しているローカライズされた文字列を検索します。 + /// + public static string AttachFileFilter { + get { + return ResourceManager.GetString("AttachFileFilter", resourceCulture); + } + } + /// /// 認証方式 に類似しているローカライズされた文字列を検索します。 /// @@ -186,6 +213,24 @@ public static string DeleteMessage { } } + /// + /// ー に類似しているローカライズされた文字列を検索します。 + /// + public static string Detach { + get { + return ResourceManager.GetString("Detach", resourceCulture); + } + } + + /// + /// 添付を削除 に類似しているローカライズされた文字列を検索します。 + /// + public static string DetachFile { + get { + return ResourceManager.GetString("DetachFile", resourceCulture); + } + } + /// /// 参照ドキュメント表示 に類似しているローカライズされた文字列を検索します。 /// @@ -249,6 +294,15 @@ public static string HistoryTokenBudget { } } + /// + /// 選択されたファイルはサポートされていない形式です。 に類似しているローカライズされた文字列を検索します。 + /// + public static string InvalidFileType { + get { + return ResourceManager.GetString("InvalidFileType", resourceCulture); + } + } + /// /// ルーム管理 に類似しているローカライズされた文字列を検索します。 /// diff --git a/SampleCSharpUI/Properties/Resources.resx b/SampleCSharpUI/Properties/Resources.resx index 25b60ad..4fd2ba1 100644 --- a/SampleCSharpUI/Properties/Resources.resx +++ b/SampleCSharpUI/Properties/Resources.resx @@ -282,4 +282,22 @@ Markdownファイル (*.md)|*.md|すべてのファイル (*.*)|*.* + + 選択されたファイルはサポートされていない形式です。 + + + ファイルを添付 + + + 画像ファイル (*.png;*.jpeg;*.jpg;*.gif;*.webp)|*.png;*.jpeg;*.jpg;*.gif;*.webp + + + + + + + + + 添付を削除 + \ No newline at end of file diff --git a/SampleCSharpUI/SampleCSharpUI.csproj b/SampleCSharpUI/SampleCSharpUI.csproj index 6596a65..738907a 100644 --- a/SampleCSharpUI/SampleCSharpUI.csproj +++ b/SampleCSharpUI/SampleCSharpUI.csproj @@ -148,6 +148,7 @@ Designer + @@ -159,6 +160,7 @@ + diff --git a/SampleCSharpUI/ViewModels/MainViewModel.cs b/SampleCSharpUI/ViewModels/MainViewModel.cs index 3cdef35..c343320 100644 --- a/SampleCSharpUI/ViewModels/MainViewModel.cs +++ b/SampleCSharpUI/ViewModels/MainViewModel.cs @@ -113,6 +113,37 @@ public string InputText } } + /// + /// 添付ファイル(1ファイルのみ) + /// + private string _AttachedFilePath; + public string AttachedFilePath + { + get => _AttachedFilePath; + set + { + if (_AttachedFilePath != value) + { + _AttachedFilePath = value; + OnPropertyChanged(); + } + } + } + + private string _AttachedFileName; + public string AttachedFileName + { + get => _AttachedFileName; + set + { + if (_AttachedFileName != value) + { + _AttachedFileName = value; + OnPropertyChanged(); + } + } + } + /// /// 参照ドキュメント一覧 /// @@ -347,13 +378,18 @@ public RelayCommand PreviewKeyDownCommand if (!string.IsNullOrEmpty(this.SelectedChatRoom?.ID)) { - //await this.Model.SendMessageAsync(this.SelectedChatRoom.ID, content); await this.Model.SendMessageStreamingAsync(this.SelectedChatRoom.ID, content); } - else + else if (string.IsNullOrEmpty(this.AttachedFilePath)) { await this.Model.SendMessageAsync(this.Messages.ToList(), (float)0.5, 1024, content); } + else + { + await this.Model.SendMessageWithFileAsync(this.Messages.ToList(), (float)0.5, 1024, content, this.AttachedFilePath); + this.AttachedFilePath = null; + this.AttachedFileName = null; + } } catch (Exception ex) { diff --git a/SampleCSharpUI/ViewModels/ManageRetrieversViewModel.cs b/SampleCSharpUI/ViewModels/ManageRetrieversViewModel.cs index 7c1fa03..e85eb49 100644 --- a/SampleCSharpUI/ViewModels/ManageRetrieversViewModel.cs +++ b/SampleCSharpUI/ViewModels/ManageRetrieversViewModel.cs @@ -49,6 +49,20 @@ public string Message } } + /// + /// リトリーバーID + /// + private string _ID = string.Empty; + public string ID + { + get { return _ID; } + set + { + _ID = value; + OnPropertyChanged(); + } + } + /// /// リトリーバー名 /// @@ -187,6 +201,7 @@ internal async Task GetRetrieversAsync() /// internal async Task GetEditDataAsync(string id) { + this.ID = id; await this.Model.GetRetrieverAsync(id); } @@ -246,13 +261,13 @@ public RelayCommand SaveCommand this.Message = "登録処理中…"; if (this.IsUseUrl || this.IsUseFile || this.IsUseFolder) { - var id = await this.Model.CreateRetrieverFromDataAsync( + this.ID = await this.Model.CreateRetrieverFromDataAsync( this.RetrieverName, this.IsUseUrl ? new List() { this.Url } : null, files); - if (!string.IsNullOrEmpty(id)) + if (!string.IsNullOrEmpty(this.ID)) { - var item = this.Model.Retrievers.FirstOrDefault((x) => x.ID == id); + var item = this.Model.Retrievers.FirstOrDefault((x) => x.ID == this.ID); if (item != null) { this.Model.SelectedRetriever = item; @@ -304,14 +319,14 @@ public RelayCommand UpdateCommand if (this.IsUseUrl || this.IsUseFile || this.IsUseFolder) { - var id = await this.Model.SetRetrieverFromDataAsync( + this.ID = await this.Model.SetRetrieverFromDataAsync( this.Model.RetrieverData, this.RetrieverName, this.IsUseUrl ? new List() { this.Url } : null, files); - if (!string.IsNullOrEmpty(id)) + if (!string.IsNullOrEmpty(this.ID)) { - var item = this.Model.Retrievers.FirstOrDefault((x) => x.ID == id); + var item = this.Model.Retrievers.FirstOrDefault((x) => x.ID == this.ID); if (item != null) { this.Model.SelectedRetriever = item; diff --git a/SampleCSharpUI/Views/EditRetrieverWindow.xaml b/SampleCSharpUI/Views/EditRetrieverWindow.xaml index 43ade6e..7bf6bc5 100644 --- a/SampleCSharpUI/Views/EditRetrieverWindow.xaml +++ b/SampleCSharpUI/Views/EditRetrieverWindow.xaml @@ -35,26 +35,29 @@ + -