Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 30 additions & 66 deletions .github/workflows/dotnet-desktop.yml
Original file line number Diff line number Diff line change
@@ -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
67 changes: 67 additions & 0 deletions .github/workflows/release-upload-zip.yml
Original file line number Diff line number Diff line change
@@ -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
33 changes: 21 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 でサインインする場合に選択 |
Expand Down Expand Up @@ -106,27 +108,34 @@ RAG自体を削除したり、RAGに格納されているデータを変更し
- 過去履歴を踏まえたプロンプトパターンの実現
- ルームへ会話を記録することによる後日閲覧の実現
- 各種トークン数の設定
しかしながら、ルーム不要でTakaneに対してプロンプトを入力し回答を得るだけでよい場合には、ルームの設定や管理などの事前実行が必要なWebAPI呼び出しは煩雑に感じてしまいます。
しかしながら、ルーム不要でTakaneに対してプロンプトを入力し回答を得るだけでよい場合には、ルームの設定や管理などの事前実行が必要なWebAPI呼び出しは煩雑に感じてしまいます。
そのようなユースケースでは「ルームなし会話」モードがあります。

### 本サンプル画面での使い方
ルームの選択で「(none)」を選択するとチャットルームなしでの利用となります。
ルームの選択で「(none)」を選択するとチャットルームなしでの利用となります。
サンプルコード内で画面に表示されている過去履歴を渡しているので、過去履歴を踏まえたやりとりが可能となっておりますが、過去履歴を渡さずにAPIコールをすれば、必ず入力したプロンプトだけを踏まえた回答となります。

#### 制限事項
ルームを「(none)」に選択してる場合は、ルーム編集や新規作成の機能はご利用になれません。
ルームを「(none)」に選択してる場合は、ルーム編集や新規作成の機能はご利用になれません。
ルーム編集やルーム新規作成が必要な場合は、他のルームを選択してから実行してください。

### ルームなし会話の実現方法
具体的には、BODYに入力されたプロンプトを設定し、下記のAPIをPOSTすることで戻り値としてAIの回答を得ることが出来ます。
### マルチモーダル対応
<img src="images/snapshot_01.png" alt="マルチモーダル対応" width="600">

ルームなし会話では、マルチモーダル対応として、添付画像に対する質問を入力することができます。
添付できる画像ファイル形式は、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 の場合は、環境設定などが必要です。

# 最後に
Expand Down
53 changes: 53 additions & 0 deletions SampleCSharpUI/Commons/APIData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }

}
}
}
70 changes: 70 additions & 0 deletions SampleCSharpUI/Commons/Base64Helper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using System;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;

namespace SampleCSharpUI.Commons
{
/// <summary>
/// 画像データをBase64エンコードするためのヘルパーメソッドを提供します。
/// </summary>
internal static class Base64Helper
{
/// <summary>
/// 指定したファイルパスの画像ファイルを非同期に読み込み、Base64エンコードした文字列を返します。
/// </summary>
/// <param name="path">読み込む画像ファイルのパス。null、空白は許可されません。</param>
/// <returns>画像データをBase64エンコードした文字列を表す非同期タスク。</returns>
/// <exception cref="ArgumentException"><paramref name="path"/> が null または空白の場合にスローされます。</exception>
/// <exception cref="FileNotFoundException">指定したパスのファイルが存在しない場合にスローされる可能性があります。</exception>
/// <exception cref="UnauthorizedAccessException">ファイルアクセス権が不足している場合にスローされる可能性があります。</exception>
/// <exception cref="NotSupportedException">ファイル サイズが int.MaxValue を超える場合にスローされます(メモリ処理不可)。</exception>
/// <remarks>
/// このメソッドはファイル全体をメモリに読み込んでからBase64変換を行います。
/// 大きなファイルを扱う場合はメモリ使用量に注意してください。
/// ファイルの部分読み込みを考慮した安全な読み取りループを使用しています。
/// </remarks>
internal static async Task<string> 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);
}
}

/// <summary>
/// 指定した URL からバイト配列を取得し、Base64 エンコードした文字列を返します。
/// </summary>
/// <param name="url">画像などのリソースを取得する URL。</param>
/// <returns>取得したデータをBase64エンコードした文字列を表す非同期タスク。</returns>
/// <exception cref="ArgumentNullException"><paramref name="url"/> が null の場合にスローされる可能性があります。</exception>
/// <exception cref="HttpRequestException">HTTP 要求の送信や応答の取得に失敗した場合にスローされます。</exception>
/// <remarks>
/// この実装では呼び出しごとに新しい <see cref="HttpClient"/> を生成しています。
/// 長時間または高頻度で使用する場合は、ソケット枯渇を避けるために <see cref="HttpClient"/> を再利用することを検討してください。
/// </remarks>
internal static async Task<string> ImageUrlToBase64Async(string url)
{
using (var http = new HttpClient())
{
var bytes = await http.GetByteArrayAsync(url).ConfigureAwait(false);
return Convert.ToBase64String(bytes);
}
}
}
}
12 changes: 10 additions & 2 deletions SampleCSharpUI/Commons/HttpHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,16 @@ private static async Task<string> 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();
}
}
}
}
Expand Down
Loading
Loading