Difyにgoogle spread sheetに連携したい

## Google Cloud Platform (GCP) で設定
GCPでGoogle Sheets APIをenableにします。
APIの認証情報(Credential)として、サービスアカウントを作成し、JSON形式のキーファイルをダウンロード(credentials.json)

## スプレッドシートの共有設定
スプレッドシートの右上の「共有」ボタンをクリックし、先ほど作成したサービスアカウントのメールアドレスを**編集者(Editor)**として追加します。

## Node.jsでライブラリをインストール
$ npm install google-auth-library googleapis

chat-sheet.js

import fs from "fs";
import fetch from "node-fetch";
import { google } from "googleapis";

// Google Sheets APIの設定
const sheets = google.sheets({ version: "v4" });
const auth = new google.auth.GoogleAuth({
  keyFile: "./credentials.json", // ダウンロードしたキーファイルのパス
  scopes: ["https://www.googleapis.com/auth/spreadsheets"],
});
const SPREADSHEET_ID = "***"; // スプレッドシートのURLから取得できるID

const DIFY_API_KEY = "app-***";
const API_URL = "https://api.dify.ai/v1/chat-messages";

const prompts = fs
  .readFileSync("./prompts.txt", "utf8")
  .split("\n")
  .map((line) => line.trim())
  .filter(Boolean);

// Difyに問い合わせる関数
async function callDify(prompt) {
  const response = await fetch(API_URL, {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${DIFY_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      inputs: {},
      query: prompt,
      response_mode: "blocking",
      conversation_id: "",
      user: "cli-user",
    }),
  });

  if (!response.ok) {
    const err = await response.text();
    throw new Error(`API Error ${response.status}: ${err}`);
  }

  const data = await response.json();
  return data.answer || "(No answer)";
}

// スプレッドシートに結果を保存する関数
async function saveToGoogleSheets(results) {
  const client = await auth.getClient();
  google.options({ auth: client });

  const values = results.map(row => [row.prompt, row.answer, row.error]);

  const resource = {
    values: [["質問", "回答", "エラー"], ...values], // ヘッダー行を追加
  };

  try {
    await sheets.spreadsheets.values.clear({
      spreadsheetId: SPREADSHEET_ID,
      range: "Sheet1!A:C", // 既存のデータをクリア(必要に応じて)
    });
    await sheets.spreadsheets.values.update({
      spreadsheetId: SPREADSHEET_ID,
      range: "Sheet1!A1",
      valueInputOption: "RAW",
      resource,
    });
    console.log("\n=== 回答をGoogleスプレッドシートに保存しました ===");
  } catch (error) {
    console.error("スプレッドシートへの書き込みエラー:", error.message);
  }
}

// メイン処理
(async () => {
  const results = [];

  for (const [i, prompt] of prompts.entries()) {
    try {
      const answer = await callDify(prompt);
      results.push({ prompt, answer, error: "" });

      // CLIに出力
      console.log(`Q${i + 1}: ${prompt}`);
      console.log(`A${i + 1}: ${answer}\n`);
    } catch (err) {
      results.push({ prompt, answer: "", error: err.message });
      console.error(`Error for "${prompt}": ${err.message}`);
    }
  }

  // Googleスプレッドシートに保存
  await saveToGoogleSheets(results);

  // (オプション)テキストファイルにも保存
  const textOutput = results
    .map((res) => `Q: ${res.prompt}\nA: ${res.answer || res.error}\n`)
    .join("\n");
  fs.writeFileSync("results.txt", textOutput, "utf8");
  console.log("=== 回答を results.txt にも保存しました ===");
})();

ほう、なるほど

Adamとloss

Adam(アダム)は、主にディープラーニングで広く使われている最適化アルゴリズムの一種。最適化アルゴリズムは、モデルの学習において、損失関数の値を最小化するためのパラメータの更新方法を決定する役割

import torch
import torch.nn as nn
import torch.optim as optim

X = torch.randn(100, 1) * 10
y = 2 * X + 1 + torch.randn(100, 1)

class LinearRegression(nn.Module):
    def __init__(self):
        super().__init__()
        self.linear = nn.Linear(1, 1)

    def forward(self, x):
        return self.linear(x)

model = LinearRegression()

# 損失関数とオプティマイザの定義
# 損失関数は平均二乗誤差
criterion = nn.MSELoss()

# オプティマイザはAdam
optimizer = optim.Adam(model.parameters(), lr=0.01)

# モデルの学習ループ
num_epochs = 1000

for epoch in range(num_epochs):
    
    # 順伝播
    y_pred = model(X)

    # 損失の計算
    loss = criterion(y_pred, y)
    
    # 逆伝播と最適化
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    
    if (epoch+1) % 100 == 0:
        print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {loss.item():.4f}')

# 学習後のパラメータ表示
weight, bias = model.linear.weight.item(), model.linear.bias.item()
print(f'Learned parameters: weight = {weight:.4f}, bias = {bias:.4f}')

勾配が急な方向には学習率を大きく、勾配が緩やかな方向には学習率を小さくすることで、効率的かつ安定して学習を進めることができる

Android: ViewModel状態管理サンプル

model/User.kt

package com.example.myapplicationstate.model

data class User {
    val id: Int,
    val name: String
}

model/UiState.kt

package com.example.myapplicationstate.model

sealed class UiState {
    object Loading: UiState()
    data class Success(val users: List<User>): UiState()
    data class Error(val message: String): UiState()
}

種類 主な目的 特徴
sealed class 型の制限つき継承(状態分岐) 継承を制限し、when式で exhaustiveness(漏れなくチェック)できる
data class データ保持専用クラス 自動で toString, equals, copy などが生成される

package com.example.myapplicationstate.viewmodel

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch

class UserViewModel : ViewModel(){
    private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
    val uiState: StateFlow<UiState> = _uiSate

    init {
        fetchUsers()
    }

    fun fetchUsers(){
        viewModelScope.launch {
            _uiState.value = UiState.Loading
            delay(2000)

            val  result = runCatching {
                getUsersFromApi()
            }

            _uiState.value =result.fold (
                onSuccess={ UiState.Success(it) },
                onFailure = { UiState.Error(it.message ?: "Unknown Error") }
            )
        }
    }

    private suspend fun getUsersFromApi(): List<User> {
        // 成功/失敗を切り替えるための仮コード
        if ((0..1).random() == 0) {
            throw RuntimeException("通信エラー")
        }

        return listOf(
            User(1, "Alice"),
            User(2, "Bob"),
            User(3, "Charlie")
        )
    }
}

ui/UserScreen.kt

package com.example.myapplicationstate.ui.screen
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.material.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import model.UiState

@Composable
fun UserScreen(userViewModel: UserViewModel = viewModel()) {
    val uiState by userViewModel.uiState.collectAsState()

    when (uiState){
        is UiState.Loading -> {
            Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
                CircularProgressIndicator()
            }
        }

        is UiState.Success -> {
            val users = (uiState as UiState.Success).users
            LazyColumn {
                items(users) { user ->
                    Text(
                        text = user.name,
                        modifier = Modifier
                            .fillMaxWidth()
                            .padding(16.dp)
                    )
                }
            }
        }

        is UiState.Error -> {
            val message = (uiState as UiSatete.Error).message
            Column (
                modifier = Modifier.fillMaxSize(),
                verticalArrangement = Arrangement.Center,
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                Text("エラー: $message", color = MaterialTheme.colors.error)
                Spacer(modifier = Modifier.height(16.dp))
                Button(onClick = { userViewModel.fetchUsers() }) {
                    Text("リトライ")
                }
            }
        }
    }
}

## @Composable

@Composable
fun Greeting(name: String) {
    Text(text = "Hello, $name!")
}
@Composable
fun MyScreen() {
    Column {
        Text("こんにちは")
        Button(onClick = { /* クリック処理 */ }) {
            Text("ボタン")
        }
    }
}

MainActivityはアプリの最初の画面
アプリのエントリーポイント

androidではuiとロジックが分けられるMVVMが一般的

[Swift] データの受け渡し: @ObservedObject

class CounterModel: ObservableObject {
    @Published var count: Int = 0
}

struct CounterPage: View {
    @ObservedObject var counter = CounterModel()
    
    var body: some View {
        VStack(spacing: 20) {
            Text("カウンター")
                .font(.title)
            
            Text("現在の値: \(counter.count)")
                .font(.headline)
                 
            Button("+1") {
                counter.count += 1
            }
            .padding()
            .background(Color.blue)
            .foregroundColor(.white)
            .cornerRadius(8)
                 
            Button("リセット") {
                counter.count = 0
            }
            .padding()
            .background(Color.red)
            .foregroundColor(.white)
            .cornerRadius(8)
            
            Spacer()
        }
        .padding()
    }
}

使い方: 小画面に渡す

NavigationLink(destination: CounterPage(counter: counter)) {
    Text("カウンターページへ")
}

小画面で受け取って使う

struct CounterPage: View {
    @ObservedObject var counter: CounterModel

    var body: some View {
        VStack {
            Text("現在の値: \(counter.count)")
            Button("+1") { counter.count += 1 }
        }
    }
}

ObservableObject クラスは「共有したいデータの所有者側」で定義・生成」 するのが基本

[Swift] データの受け渡し :bindingと値渡し

ContentView.swift

                NavigationLink(destination: CounterPage(message: userMessage)) {
                    Text("カウンターページへ")
                        .foregroundColor(.white)
                        .padding()
                        .background(Color.green)
                        .cornerRadius(8)
                }

CounterPage.swift

struct CounterPage: View {
    let message: String
    @State private var count = 0
    
    var body: some View {
        VStack(spacing: 20) {
            Text("受け取ったメッセージ:")
                        Text(message)   // ← 表示して確認
                            .font(.title)
                            .foregroundColor(.blue)
 // 省略

ContentView.swiftで、api側にデータを送る際には、 TextField(“メッセージを入力”, text: $userMessage) としているのに、今回は NavigationLink(destination: CounterPage(message: userMessage)) としている

「text: $userMessage」と「message: userMessage」の違いは バインディング(Binding)か値のコピーか

1. text: $userMessage
$ をつけると バインディング(Binding) を渡している。
バインディングは「元の変数と直接つながっている参照」のようなもの。
TextField の入力が変わると、自動的に @State var userMessage の値も更新される。

👉 双方向のデータやり取りが可能。
例:入力欄に文字を打つと userMessage が変わるし、逆に userMessage を変えても入力欄が変わる。

2. message: userMessage
$ がついていないので、ただの 値のコピー を渡している。
遷移先 CounterPage の message: String に「現在の値」を渡すだけ。
遷移先で message を書き換えても、元の userMessage には影響しない。

元の画面で変更内容を反映させたい時などはbindingの方が良い

DifyのチャットフローにAPIで複数質問を送信して、テキストで保存する

import fs from "fs";
import fetch from "node-fetch";

const DIFY_API_KEY = "app-*";
const API_URL = "https://api.dify.ai/v1/chat-messages";

// テキストファイルから質問を読み込む
const prompts = fs
  .readFileSync("./prompts.txt", "utf8")
  .split("\n")
  .map((line) => line.trim())
  .filter(Boolean);

// Difyに問い合わせる関数
async function callDify(prompt) {
  const response = await fetch(API_URL, {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${DIFY_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      inputs: {},
      query: prompt,
      response_mode: "blocking",
      conversation_id: "",
      user: "cli-user",
    }),
  });

  if (!response.ok) {
    const err = await response.text();
    throw new Error(`API Error ${response.status}: ${err}`);
  }

  const data = await response.json();
  return data.answer || "(No answer)";
}

// メイン処理
(async () => {
  const results = [];
  results.push("=== 質問と回答 ===\n");

  for (const [i, prompt] of prompts.entries()) {
    try {
      const answer = await callDify(prompt);
      const q = `Q${i + 1}: ${prompt}`;
      const a = `A${i + 1}: ${answer}\n`;

      // CLIに出力
      console.log(q);
      console.log(a);

      // ファイル保存用にも追加
      results.push(q, a);
    } catch (err) {
      const e = `Error for "${prompt}": ${err.message}`;
      console.error(e);
      results.push(e);
    }
  }

  // ファイルに保存
  fs.writeFileSync("results.txt", results.join("\n"), "utf8");
  console.log("\n=== 回答を results.txt に保存しました ===");
})();

prompt.txt

Node.jsでAPIリクエストを送る方法を教えてください。
ReactとVueの違いは何ですか?
GPTとBERTの違いを説明してください。

result.txt

=== 質問と回答 ===

Q1: Node.jsでAPIリクエストを送る方法を教えてください。
A1: うむ、それは簡単じゃ。まず、"axios"というライブラリをインストールせんとな。次に「axios.get("リクエスト先のURL")」というコードを書く。これでAPIにリクエストを送れるぞ。

Q2: ReactとVueの違いは何ですか?
A2: ふむ、ReactとVueか。ReactはFacebook製で大規模開発に向いており、Vueはやさしく始められる。しかし、どちらも優れたツールだ。お主が何を求めているか、それによる。

Q3: GPTとBERTの違いを説明してください。
A3: うむ、そなたが知識を求める姿勢は評価するぞ。GPTとは文章を生成するためのモデルで、一方、BERTは文章を理解するためのものじゃ。それぞれの目的に応じて、我々は適切な道具を選ぶべきじゃ。

Difyのチャットフローにサーバ側からAPIリクエストでテストしたい

node.js

const DIFY_API_KEY = "";
const WORKFLOW_ID = "";

// APIエンドポイントのURL
const url = "https://api.dify.ai/v1/chat-messages";

// APIリクエストの実行
async function runDifyChat() {
  try {
    const response = await fetch(url, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "Authorization": `Bearer ${DIFY_API_KEY}` // APIキーをAuthorizationヘッダーに設定
      },
      body: JSON.stringify({
        "inputs": {}, // 必要に応じて入力データを設定
        "query": "Node.jsでAPIリクエストを送る方法を教えてください。", // ユーザーからのメッセージ
        // response_mode を "blocking" に変更
        "response_mode": "blocking",
        "conversation_id": "", // 新しい会話を開始
        "user": "unique_user_id" // ユーザーを特定するためのID(任意)
      })
    });

    if (!response.ok) {
      const errorData = await response.json();
      throw new Error(`API Error: ${response.status} - ${JSON.stringify(errorData)}`);
    }

    // blockingモードなら、response.json() で直接パースできる
    const data = await response.json();
    console.log("APIレスポンス:", data);

  } catch (error) {
    console.error("エラーが発生しました:", error);
  }
}

// 関数を実行
runDifyChat();

$ node chat.js
APIレスポンス: {
event: ‘message’,
task_id: ’52ba8605-bac3-46b4-a0aa-5958079a3d01′,
id: ‘d858017a-052d-4a1b-86eb-da673707423e’,
message_id: ‘d858017a-052d-4a1b-86eb-da673707423e’,
conversation_id: ’37feb5fc-5694-48ae-ab86-e45fae25aaa9′,
mode: ‘advanced-chat’,
answer: ‘まずは「axios」というライブラリを用いると良い。以下のコードを参考にせんといかん。\n’ +
‘“`javascript\n’ +
“const axios = require(‘axios’);\n” +
“axios.get(‘APIのURL’)\n” +
‘ .then(response => {\n’ +
‘ console.log(response.data);\n’ +
‘ })\n’ +
‘ .catch(error => {\n’ +
‘ console.error(error);\n’ +
‘ });\n’ +
‘“`\n’ +
‘上記はGETリクエストの例で、POSTリクエストを送る際は`axios.get`の部分を`axios.post`に変え、第二引数に送りたいデータをオブジェクトとして渡せばよい。’,
metadata: {
annotation_reply: null,
retriever_resources: [ [Object], [Object], [Object] ],
usage: {
prompt_tokens: 2379,
prompt_unit_price: ‘0.03’,
prompt_price_unit: ‘0.001’,
prompt_price: ‘0.049953’,
completion_tokens: 206,
completion_unit_price: ‘0.06’,
completion_price_unit: ‘0.001’,
completion_price: ‘0.0091425’,
total_tokens: 2585,
total_price: ‘0.0590955’,
currency: ‘USD’,
latency: 2.760161219164729
}
},
created_at: 1757759242
}

Jetpack Compose + ViewMode

build.gradle.kts

plugins {
    alias(libs.plugins.android.application)
    alias(libs.plugins.kotlin.android)
    alias(libs.plugins.kotlin.compose)
}

android {
    namespace = "com.example.myapplicationstate"
    compileSdk = 36

    defaultConfig {
        applicationId = "com.example.myapplicationstate"
        minSdk = 24
        targetSdk = 36
        versionCode = 1
        versionName = "1.0"

        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            isMinifyEnabled = false
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_11
        targetCompatibility = JavaVersion.VERSION_11
    }
    kotlinOptions {
        jvmTarget = "11"
    }
    buildFeatures {
        compose = true
    }
}

dependencies {

    implementation(libs.androidx.core.ktx)
    implementation(libs.androidx.lifecycle.runtime.ktx)
    implementation(libs.androidx.activity.compose)
    implementation(platform(libs.androidx.compose.bom))
    implementation(libs.androidx.ui)
    implementation(libs.androidx.ui.graphics)
    implementation(libs.androidx.ui.tooling.preview)
    implementation(libs.androidx.material3)
    testImplementation(libs.junit)
    androidTestImplementation(libs.androidx.junit)
    androidTestImplementation(libs.androidx.espresso.core)
    androidTestImplementation(platform(libs.androidx.compose.bom))
    androidTestImplementation(libs.androidx.ui.test.junit4)

    implementation(libs.androidx.lifecycle.viewmodel.compose)
    debugImplementation(libs.androidx.ui.tooling)
    debugImplementation(libs.androidx.ui.test.manifest)
}

MainActivity.kt

package com.example.myapplicationstate

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.myapplicationstate.ui.CounterScreen

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                Surface {
                    CounterScreen()
                }
            }
        }
    }
}

MainViewModel.kt

package com.example.myapplicationstate.ui.theme

import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.myapplicationstate.MainViewModel

@Composable
fun CounterScreen(viewModel: MainViewModel = viewModel()) {
    val count by viewModel.count.collectAsState()

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(32.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Text(text = "カウント: $count", style = MaterialTheme.typography.headlineMedium)
 

Androidのstate

XMLレイアウト + Retrofit通信

activity_main.xml

<TextView
    android:id="@+id/counterText"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="カウント: 0"
    android:textSize="20sp"
    android:layout_marginTop="16dp" />

<Button
    android:id="@+id/incrementButton"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="カウントアップ" />

MainActivity

package com.example.myapplication

import com.example.myapplication.ChatApi
import com.example.myapplication.ChatRequest
import com.example.myapplication.ChatResponse
import com.example.myapplication.ApiClient


import android.os.Bundle
import android.widget.Button
import android.widget.EditText
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response

class MainActivity : AppCompatActivity() {

    private var count = 0

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val inputMessage = findViewById<EditText>(R.id.inputMessage)
        val sendButton = findViewById<Button>(R.id.sendButton)
        val resultText = findViewById<TextView>(R.id.resultText)

        val counterText = findViewById<TextView>(R.id.counterText)
        val incrementButton = findViewById<Button>(R.id.incrementButton)

        sendButton.setOnClickListener {
            val message = inputMessage.text.toString()
            val request = ChatRequest(name = "田中", job = "エンジニア")

            ApiClient.chatApi.sendMessage(request).enqueue(object : Callback<ChatResponse> {
                override fun onResponse(call: Call<ChatResponse>, response: Response<ChatResponse>) {
                    if (response.isSuccessful) {
                        val body = response.body()
                        resultText.text = if (body != null) {
                            "name: ${body.name}\njob: ${body.job}\nid: ${body.id}\ncreatedAt: ${body.createdAt}"
                        } else {
                            "応答なし"
                        }
                    } else {
                        resultText.text = "エラーコード: ${response.code()}"
                    }
                }

                override fun onFailure(call: Call<ChatResponse>, t: Throwable) {
                    resultText.text = "通信失敗: ${t.localizedMessage}"
                }
            })
        }
        
        incrementButton.setOnClickListener {
            count++
            counterText.text = "カウント: $count"
        }
    }
}

特徴 XMLレイアウト + Retrofit通信 Jetpack Compose
UIの作り方 XMLファイルで定義 Kotlinコードで定義
表示の変更 TextView.setText()などで明示的に更新 状態が変われば自動で再描画される
直感性 複雑で古いUIも対応できるが手間 シンプルでモダンなUIが書きやすい
可読性・保守性 XMLとKotlinが分かれて見づらくなる すべてKotlinに書けるので読みやすい
プレビュー Android Studioでリアルタイムプレビュー可能 同じく可(Compose Preview)

モダンな描き方はjetpack composeが良さそう

Androidの次の学習項目

⭐️ 高 ViewModel + State管理 複雑なUI状態を安全に管理。画面回転にも強い。
⭐️ 高 Repositoryパターン RetrofitやRoomとの接続を分離して保守性UP
⭐️ 高 Room(ローカルDB) オフライン保存・データ永続化の基礎
⭐️ 高 データの非同期処理(Coroutines / Flow) 通信やDB操作を効率的に行う。非同期処理の本命
⭐️ 中 UIのアニメーション Composeのanimate*関数で滑らかなUI体験
⭐️ 中 Dependency Injection(Hilt) テストしやすい、保守しやすい設計にするための基盤
⭐️ 中 Jetpack Compose Navigation(複雑版) 引数付き遷移、戻る処理、BottomNavなどを扱う
⭐️ 低〜中 テスト(Unit, UIテスト) 安定したアプリを作るには重要。ただし最初は後回しでもOK
⭐️ 低 デザインパターン(MVVMなど) 設計力を高めたいときに学習

いまいちピンとこないものが多いですが、結構ありますね。