Repository: HACK-U-2025-2/HACKU
Pull request #240: feat: generate_tags関数の追加
Author: login: ootake430
Created: 2025-05-28T01:01:38Z | Merged: 2025-05-29T14:33:52Z

close #239 
relate #104 

## generate_tags関数の追加
メモ(body)からメジャータグおよびカスタムタグを抽出し、1つのリストとして返すgenerate_tags関数を追加しました。
本関数は内部で2つのサブ関数（extract_major_tags, extract_custom_tags）を呼び出し、それぞれLLMプロンプトを分けてタグ抽出処理を行います。
サブ関数は外部から呼び出されることがないため、generate_tags内に関数として定義しています。

- メジャータグ：所定のカテゴリから該当タグを抽出
- カスタムタグ：本文中の固有名詞等を抽出
- 両者を結合し、重複を除いたリストとして返却

Linked issue #239: sprint2:[機械学習] タグ自動生成関数の追加

Diff:
diff --git a/backend/llm/generate_tags.py b/backend/llm/generate_tags.py
new file mode 100644
index 0000000..4ca40b8
--- /dev/null
+++ b/backend/llm/generate_tags.py
@@ -0,0 +1,164 @@
+from llm.utils.generator import generate_text
+from llm.utils.parser import parse_json
+
+
+def generate_tags(
+    new_text: str,
+    initial_tags: list[str] = None,
+    related_text: str = None,  # ここをstr型に
+    related_tags: list[str] = None,  # ここをlist[str]型に
+) -> list[str]:
+    """
+    メモ追加時のタグ生成アシスタント関数。
+    """
+    initial_tags = initial_tags or []
+    related_text = related_text or ""
+    related_tags = related_tags or []
+    already_tags = set(initial_tags)
+
+    # tags（除外したいタグ）が空でなければ、それらを除外するようLLMへの指示文を設計する
+    def format_exclude_tags_instruction(tags: set[str]) -> str:
+        if tags:
+            return f"- 既に含まれるタグ（{', '.join(tags)}）は除外してください。"
+        return ""
+
+    # メジャータグ抽出
+    def extract_major_tags(text: str, exclude_tags: set[str]) -> list[str]:
+        exclude_instruction = format_exclude_tags_instruction(exclude_tags)
+        prompt = f"""
+# 指示
+あなたはメモ分類アシスタントです。
+以下のメモ本文から、該当する「メジャータグ」を**必要なだけ全て**選んでください。
+- メジャータグ：「アイデア、日記、ToDo、イベント」
+{exclude_instruction}
+
+# 出力形式 該当がなければ空リスト
+{{"result": "タグ1, タグ2, タグ3"}}
+
+# メモ本文
+{text}
+"""
+        tag_str = parse_json(
+            generate_text(prompt, enable_thinking=False, max_new_tokens=128)
+        )
+        if isinstance(tag_str, str):
+            return [t.strip() for t in tag_str.split(",") if t.strip()]
+        if isinstance(tag_str, list):
+            return [t.strip() for t in tag_str if t.strip()]
+        return []
+
+    # カスタムタグ抽出
+    def extract_custom_tags(text: str, exclude_tags: set[str]) -> list[str]:
+        exclude_instruction = format_exclude_tags_instruction(exclude_tags)
+        prompt = f"""
+# 指示
+あなたはメモ分類アシスタントです。
+以下のメモ本文から、人名以外の固有名詞（例：地名、商品名、施設名、イベント名、キーワードなど）をすべて抽出してください。
+{exclude_instruction}
+
+# 出力形式 該当がなければ空リスト
+{{"result": "タグ1, タグ2, タグ3"}}
+
+# メモ本文
+{text}
+"""
+        tag_str = parse_json(
+            generate_text(prompt, enable_thinking=False, max_new_tokens=128)
+        )
+        if isinstance(tag_str, str):
+            return [t.strip() for t in tag_str.split(",") if t.strip()]
+        if isinstance(tag_str, list):
+            return [t.strip() for t in tag_str if t.strip()]
+        return []
+
+    # アイデア種別分類
+    def classify_idea_type(text: str) -> str:
+        prompt = f"""
+# 指示
+次のメモがどの分野やカテゴリの「アイデア」か、1つの短い単語で答えてください。
+例：「アプリ」「サービス」「イベント」「商品」「仕事」「企画」「研究」「デザイン」「ストーリー」など
+該当するものがない場合は"None"を返してください。
+
+# メモ本文
+{text}
+
+# 出力形式
+例1: {{"result": "健康"}}
+例2: {{"result": "None"}}
+"""
+        category = parse_json(
+            generate_text(prompt, enable_thinking=False, max_new_tokens=64)
+        )
+        if isinstance(category, str):
+            cat = category.strip()
+            if cat and cat.lower() != "none":
+                return cat
+        return ""
+
+    # 関連メモ由来タグの提案（ここを単一に変更）
+    def suggest_related_tags(
+        text: str, rel_text: str, rel_tags: list[str], exclude_tags: set[str]
+    ) -> list[str]:
+        if not rel_text or not rel_tags:
+            return []
+        exclude_instruction = format_exclude_tags_instruction(exclude_tags)
+        formatted = f"- 本文: {rel_text}\n  タグ: {', '.join(rel_tags)}"
+        prompt = f"""
+# 指示
+あなたはメモ分類アシスタントです。
+追加されるメモ本文と関連メモの情報を参考に、
+**追加すべきタグ**を必要なだけ提案してください。
+{exclude_instruction}
+
+# 追加されるメモ本文
+{text}
+
+# 関連メモ
+{formatted}
+
+# 出力形式
+{{"result": "タグ1, タグ2, タグ3"}}
+"""
+        tag_str = parse_json(
+            generate_text(prompt, enable_thinking=False, max_new_tokens=128)
+        )
+        if isinstance(tag_str, str):
+            return [t.strip() for t in tag_str.split(",") if t.strip()]
+        if isinstance(tag_str, list):
+            return [t.strip() for t in tag_str if t.strip()]
+        return []
+
+    # メインのタグ生成処理
+    tags_result: list[str] = list(initial_tags)
+
+    # メジャータグ
+    major_tags = extract_major_tags(new_text, already_tags)
+    tags_result += [t for t in major_tags if t not in already_tags]
+    already_tags.update(major_tags)
+
+    # アイデア区分追加
+    # initial_tagsに「アイデア」が含まれる場合は「アイデア」を残して区分を追加、major_tagsに含まれる場合は置換
+    idea_in_initial = "アイデア" in (initial_tags or [])
+    idea_in_major = "アイデア" in major_tags
+
+    if idea_in_initial or idea_in_major:
+        idea_category = classify_idea_type(new_text)
+        if idea_category and idea_category not in already_tags:
+            tags_result.append(idea_category)
+            already_tags.add(idea_category)
+        # 置換はmajor_tagsに「アイデア」があってinitial_tagsにない場合のみ
+        if idea_in_major and not idea_in_initial:
+            tags_result = [t for t in tags_result if t != "アイデア"]
+
+    # カスタムタグ
+    custom_tags = extract_custom_tags(new_text, already_tags)
+    tags_result += [t for t in custom_tags if t not in already_tags]
+    already_tags.update(custom_tags)
+
+    # 関連メモタグ
+    related_suggestions = suggest_related_tags(
+        new_text, related_text, related_tags, already_tags
+    )
+    tags_result += [t for t in related_suggestions if t not in already_tags]
+
+    return tags_result
diff --git a/backend/tests/test_put_memo_websocket.py b/backend/tests/test_put_memo_websocket.py
index 3f6978e..ac0b927 100644
--- a/backend/tests/test_put_memo_websocket.py
+++ b/backend/tests/test_put_memo_websocket.py
@@ -28,7 +28,7 @@ def test_normal_update_websocket(test_db, client):
         assert response["status"] == "success"
         assert response["memo_id"] == memo_id
 
-        asyncio.run(asyncio.sleep(1.2))
+        asyncio.run(asyncio.sleep(3.2))
 
         websocket.close()
 


Reviews:
user: login: Mattya3
body: コメントしました。対応お願いします。
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-05-28T02:28:52Z
user: login: tyPhoon-collab
body: 実装ありがとうございます！以前デモを見せていただいた感じでは良さそうだったので良いと思います。サーバー側で合意が取れたらマージしてください🙏
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-28T03:29:37Z
user: login: ootake430
body: コメント対応しました
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-29T01:59:23Z
user: login: Mattya3
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-29T04:37:50Z
user: login: ootake430
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-29T05:45:53Z
user: login: ootake430
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-29T10:23:19Z
user: login: ootake430
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-29T10:44:52Z
user: login: ootake430
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-29T10:51:20Z
user: login: Mattya3
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-29T14:33:43Z

Review comments:
diff_hunk: @@ -0,0 +1,61 @@
+from llm.utils.generator import generate_text
+from llm.utils.parser import parse_json
+
+
+def generate_tags(text: str) -> list[str]:
+    # メジャータグ抽出
+    def extract_major_tags(text: str) -> list[str]:
+        prompt = f"""
+# 指示
+あなたはメモ分類アシスタントです。
+以下のメモ本文から、該当する「メジャータグ」を**必要なだけ全て**選んでください。
+- メジャータグ：「アイデア、ヒント、日記、ToDo、旅行、食事、人間関係、イベント、買い物、予定」
path: backend/llm/generate_tags.py
user: login: Mattya3
body: メジャー・マイナーで分ける意図は何でしょう。

アイデアを記録するアプリがテーマなのに対してタグに”アイデア”があるのが気になります(この分類だとアイデアに特化できていない気がします)。

特化させるのなら以下のようなリストになると思います。
ストーリー、映像・音声、研究・分析、製品・サービス、プロジェクト案、ライフデザイン、デザイン、日記、TODO、イベント
こんにちは！
created_at: 2025-05-28T02:11:12Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,61 @@
+from llm.utils.generator import generate_text
+from llm.utils.parser import parse_json
+
+
+def generate_tags(text: str) -> list[str]:
+    # メジャータグ抽出
+    def extract_major_tags(text: str) -> list[str]:
+        prompt = f"""
+# 指示
+あなたはメモ分類アシスタントです。
+以下のメモ本文から、該当する「メジャータグ」を**必要なだけ全て**選んでください。
+- メジャータグ：「アイデア、ヒント、日記、ToDo、旅行、食事、人間関係、イベント、買い物、予定」
+
+# 出力形式 該当がなければ空リスト
+{{"result": "タグ1, タグ2, タグ3"}}
+
+# メモ本文
+{text}
+"""
+        json_text = generate_text(prompt, enable_thinking=False, max_new_tokens=128)
+        # parse_jsonの返り値はカンマ区切りの文字列前提
+        tag_str = parse_json(json_text)
+        if isinstance(tag_str, str):
+            tags = [t.strip() for t in tag_str.split(",") if t.strip()]
+        elif isinstance(tag_str, list):  # 念のためリスト形式にも対応
+            tags = [t.strip() for t in tag_str if t.strip()]
+        else:
+            tags = []
+        return tags
+
+    # カスタムタグ抽出
+    def extract_custom_tags(text: str) -> list[str]:
+        prompt = f"""
+# 指示
+あなたはメモ分類アシスタントです。
+以下のメモ本文から、「カスタムタグ」として本文中に現れる固有名詞や個人に紐づく表現（人名、地名、商品名、施設名、個別ワードなど）を全て抽出してください。
path: backend/llm/generate_tags.py
user: login: Mattya3
body: この記載だと、肝心な”抽象度の高いアイデア”に対し有効に働かない気がします。

適当に考えたものですが以下はどうでしょうか

本文中に現れる実在・想像を問わず重要な固有名詞や固有表現（地名、商品名、施設名、架空設定、特有のキーワードなど）

さすがに人名(実名の場合)はセキュリティの都合上タグに乗せるべきではないかと思います(セキュリティ用に例外処理を追加する時間はないため)。
created_at: 2025-05-28T02:25:18Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,61 @@
+from llm.utils.generator import generate_text
+from llm.utils.parser import parse_json
+
+
+def generate_tags(text: str) -> list[str]:
+    # メジャータグ抽出
+    def extract_major_tags(text: str) -> list[str]:
+        prompt = f"""
+# 指示
+あなたはメモ分類アシスタントです。
+以下のメモ本文から、該当する「メジャータグ」を**必要なだけ全て**選んでください。
+- メジャータグ：「アイデア、ヒント、日記、ToDo、旅行、食事、人間関係、イベント、買い物、予定」
+
+# 出力形式 該当がなければ空リスト
+{{"result": "タグ1, タグ2, タグ3"}}
+
+# メモ本文
+{text}
+"""
+        json_text = generate_text(prompt, enable_thinking=False, max_new_tokens=128)
+        # parse_jsonの返り値はカンマ区切りの文字列前提
+        tag_str = parse_json(json_text)
+        if isinstance(tag_str, str):
+            tags = [t.strip() for t in tag_str.split(",") if t.strip()]
+        elif isinstance(tag_str, list):  # 念のためリスト形式にも対応
+            tags = [t.strip() for t in tag_str if t.strip()]
+        else:
+            tags = []
+        return tags
+
+    # カスタムタグ抽出
+    def extract_custom_tags(text: str) -> list[str]:
+        prompt = f"""
+# 指示
+あなたはメモ分類アシスタントです。
+以下のメモ本文から、「カスタムタグ」として本文中に現れる固有名詞や個人に紐づく表現（人名、地名、商品名、施設名、個別ワードなど）を全て抽出してください。
+
+# 出力形式 該当がなければ空リスト
+{{"result": "タグ1, タグ2, タグ3"}}
+
+# メモ本文
+{text}
+"""
+        json_text = generate_text(prompt, enable_thinking=False, max_new_tokens=128)
+        tag_str = parse_json(json_text)
+        if isinstance(tag_str, str):
+            tags = [t.strip() for t in tag_str.split(",") if t.strip()]
+        elif isinstance(tag_str, list):
+            tags = [t.strip() for t in tag_str if t.strip()]
+        else:
+            tags = []
+        return tags
+
+    # 実装が間に合えばここに、関連メモを取得 -> プロンプトで参照 -> 新たなタグ候補を取得するプロセスを追加
+
+    # タグ取得
+    major_tags = extract_major_tags(text)
+    custom_tags = extract_custom_tags(text)
+    # 重複排除
path: backend/llm/generate_tags.py
user: login: Mattya3
body: メモが長いほど大量にタグが生成されると思いますが、何かしらのロジックで削る必要があるかと
created_at: 2025-05-28T02:26:54Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,61 @@
+from llm.utils.generator import generate_text
+from llm.utils.parser import parse_json
+
+
+def generate_tags(text: str) -> list[str]:
path: backend/llm/generate_tags.py
user: login: Mattya3
body: 元々のアイデアだと以下の流れになっていたかと思います。

①ユーザが作成したメモの、ボディ・タグの組を1件取得する
②llmにその組を与え、付けるべきタグリストを考えてもらう(生成タグ)
③タグリストのタグそれぞれを類似度検索し、上位1個(類似タグ)取得する
④類似度を比較し、閾値以上なら代替
⑤決定したタグのリストを登録

大きく変わっていると思いますが、この実装はどのような流れを想定していますか？

また、これを使用するエンドポイントでは、ユーザがボディに対して作成したタグも受け取ります。
ここと競合する可能性があるのですが、このタグはどのように扱いますか？
created_at: 2025-05-28T02:28:29Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,61 @@
+from llm.utils.generator import generate_text
+from llm.utils.parser import parse_json
+
+
+def generate_tags(text: str) -> list[str]:
path: backend/llm/generate_tags.py
user: login: ootake430
body: ① 新規作成メモに類似している既存メモとそれに紐づくタグ群を取得 (関連メモ表示のvector検索を利用)既存メモとそれに紐づくタグ群は１件(top1)とする
-> 関連メモベクトル検索関数が未実装のため、実装され次第追記

**② ①で得られたメモ(body)とタグをプロンプトに入れて参照させながら、新規メモのbodyからgenerate_tagsでタグ生成**

③ 生成タグ群に対して既存タグチェック関数を通して、閾値以上の場合に既存タグに変換(表現を変換)、閾値未満は新規タグとし、タグリストを得る (LLM判定でも可、LLM判定の方は余力があれば実装 (閾値による変換判定と結果がほぼ変わらない可能性が高いため))

④タグリストをDBに追加、フロントに返却+表示

凡その流れはnotionに記載していたものと変わってないはずです


created_at: 2025-05-28T02:54:31Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,61 @@
+from llm.utils.generator import generate_text
+from llm.utils.parser import parse_json
+
+
+def generate_tags(text: str) -> list[str]:
+    # メジャータグ抽出
+    def extract_major_tags(text: str) -> list[str]:
+        prompt = f"""
+# 指示
+あなたはメモ分類アシスタントです。
+以下のメモ本文から、該当する「メジャータグ」を**必要なだけ全て**選んでください。
+- メジャータグ：「アイデア、ヒント、日記、ToDo、旅行、食事、人間関係、イベント、買い物、予定」
path: backend/llm/generate_tags.py
user: login: ootake430
body: > メジャー・マイナーで分ける意図は何でしょうか。

メモにつけるタグには、大きく一般的な分類（メジャータグ）とローカルなキーワード（マイナータグ/カスタムタグ）の2種類があると考えています。
前者は多くのメモに共通して使える抽象度の高いタグであり、一定のルールで分類が可能です。一方で、後者は固有名詞や特定の状況に依存する個別的な表現が該当します。

この2種類を分けて取得する設計にしている理由は、メジャータグによる分類を行うことで、後段の削除推定関数などの判定精度向上が期待できるためです。共通の分類軸（メジャータグ）を持つことで、汎用的な処理や分析が容易になり、メモ管理の最適化につながると考えています。
created_at: 2025-05-28T02:58:10Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,61 @@
+from llm.utils.generator import generate_text
+from llm.utils.parser import parse_json
+
+
+def generate_tags(text: str) -> list[str]:
+    # メジャータグ抽出
+    def extract_major_tags(text: str) -> list[str]:
+        prompt = f"""
+# 指示
+あなたはメモ分類アシスタントです。
+以下のメモ本文から、該当する「メジャータグ」を**必要なだけ全て**選んでください。
+- メジャータグ：「アイデア、ヒント、日記、ToDo、旅行、食事、人間関係、イベント、買い物、予定」
path: backend/llm/generate_tags.py
user: login: ootake430
body: > アイデアを記録するアプリがテーマなのに対してタグに”アイデア”があるのが気になります(この分類だとアイデアに特化できていない気がします)。

メモのサンプルから、特段区別の必要性を感じなかったのでこうしてましたが、コンセプト的にはそれが適切ですね、アイデアタグが存在する場合に、アイデアの区分を行うようにしたいと思います
created_at: 2025-05-28T03:00:29Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,61 @@
+from llm.utils.generator import generate_text
+from llm.utils.parser import parse_json
+
+
+def generate_tags(text: str) -> list[str]:
+    # メジャータグ抽出
+    def extract_major_tags(text: str) -> list[str]:
+        prompt = f"""
+# 指示
+あなたはメモ分類アシスタントです。
+以下のメモ本文から、該当する「メジャータグ」を**必要なだけ全て**選んでください。
+- メジャータグ：「アイデア、ヒント、日記、ToDo、旅行、食事、人間関係、イベント、買い物、予定」
+
+# 出力形式 該当がなければ空リスト
+{{"result": "タグ1, タグ2, タグ3"}}
+
+# メモ本文
+{text}
+"""
+        json_text = generate_text(prompt, enable_thinking=False, max_new_tokens=128)
+        # parse_jsonの返り値はカンマ区切りの文字列前提
+        tag_str = parse_json(json_text)
+        if isinstance(tag_str, str):
+            tags = [t.strip() for t in tag_str.split(",") if t.strip()]
+        elif isinstance(tag_str, list):  # 念のためリスト形式にも対応
+            tags = [t.strip() for t in tag_str if t.strip()]
+        else:
+            tags = []
+        return tags
+
+    # カスタムタグ抽出
+    def extract_custom_tags(text: str) -> list[str]:
+        prompt = f"""
+# 指示
+あなたはメモ分類アシスタントです。
+以下のメモ本文から、「カスタムタグ」として本文中に現れる固有名詞や個人に紐づく表現（人名、地名、商品名、施設名、個別ワードなど）を全て抽出してください。
path: backend/llm/generate_tags.py
user: login: ootake430
body: 微修正して、後程サンプルも上げますね
"抽象度の高いアイデア"のテキストと取得したいタグリストのサンプルがいくつかあればうれしいです
created_at: 2025-05-28T03:02:52Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,61 @@
+from llm.utils.generator import generate_text
+from llm.utils.parser import parse_json
+
+
+def generate_tags(text: str) -> list[str]:
path: backend/llm/generate_tags.py
user: login: ootake430
body: おそらく、メモ新規作成時ではなく、メモ詳細画面(?)に入ってタグを追加したときのことですよね？
その時点ではすでに生成されたタグを見れる状態でタグ追加することになると想定していて
タグ追加は、既存タグチェック関数(タグのベクトル検索)を通すのみで良いかなと思います
created_at: 2025-05-28T03:54:46Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,61 @@
+from llm.utils.generator import generate_text
+from llm.utils.parser import parse_json
+
+
+def generate_tags(text: str) -> list[str]:
path: backend/llm/generate_tags.py
user: login: Mattya3
body: > おそらく、メモ新規作成時ではなく、メモ詳細画面(?)に入ってタグを追加したときのことですよね？

ゼミ中なのでこちらの回答だけ，
sprint1では自動タグ生成を実装しなかったため，代わりにpost時にユーザが生成したタグを受け取れるようになっています．
フロントではsprint1でタグの入力欄を実装していないっぽいですが，タグを受け取れる都合上，競合しないような対処が必要です．

なお，メモ詳細画面からのタグの追加(PATCH)では，特に何か対処はいらないかと思います．
created_at: 2025-05-29T04:37:50Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,61 @@
+from llm.utils.generator import generate_text
+from llm.utils.parser import parse_json
+
+
+def generate_tags(text: str) -> list[str]:
path: backend/llm/generate_tags.py
user: login: ootake430
body: OMG
タグもgenerate_tagsに入れれるようにして、ユーザー入力タグを除いて生成するように修正しようと思います。
ユーザー入力タグは変更せずそのままgenerate_tagsの出力として返すようにしますね
created_at: 2025-05-29T05:45:53Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,61 @@
+from llm.utils.generator import generate_text
+from llm.utils.parser import parse_json
+
+
+def generate_tags(text: str) -> list[str]:
+    # メジャータグ抽出
+    def extract_major_tags(text: str) -> list[str]:
+        prompt = f"""
+# 指示
+あなたはメモ分類アシスタントです。
+以下のメモ本文から、該当する「メジャータグ」を**必要なだけ全て**選んでください。
+- メジャータグ：「アイデア、ヒント、日記、ToDo、旅行、食事、人間関係、イベント、買い物、予定」
path: backend/llm/generate_tags.py
user: login: ootake430
body: 「アイデア、日記、ToDo、イベント」
created_at: 2025-05-29T10:23:19Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,61 @@
+from llm.utils.generator import generate_text
+from llm.utils.parser import parse_json
+
+
+def generate_tags(text: str) -> list[str]:
path: backend/llm/generate_tags.py
user: login: ootake430
body: ①ユーザが作成したメモの、ボディ・タグの組を1件取得する
②llmにその組を与え、付けるべきタグリストを考えてもらう(生成タグ)
③タグリストのタグそれぞれを類似度検索し、上位k個(類似タグ)取得する
~~④llmに生成タグと類似タグの組を渡し、どちらを採用するかを決定~~
⑤決定したタグのリストを登録
created_at: 2025-05-29T10:44:52Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,61 @@
+from llm.utils.generator import generate_text
+from llm.utils.parser import parse_json
+
+
+def generate_tags(text: str) -> list[str]:
path: backend/llm/generate_tags.py
user: login: ootake430
body: generate_tags(new_body, new_tags, relate_body, relate_tags)

アイデアは変換出来たら消す、できなかったら残す
created_at: 2025-05-29T10:51:19Z
author_association: COLLABORATOR

PR comments:
user: login: ootake430
created_at: 2025-05-28T07:21:13Z
body: @Mattya3 
- アイデア種別分類ロジックを追加実装しました
- 固有名詞として人名は含めないようプロンプトを変更しました
- 取得タグ数については、実運用上でどれほど長い文章がやってくるのかの寄りますね、future workとします


プロンプトレベルの修正は別issueで対応します。
画像ですがこんな感じでタグが付きます、本番環境では要約後テキストを用いるのでもう少しスマートになるかと
精度は８割満足って感じですね、プロンプトをこれ以上複雑するとJSON出力をしなくなったりと破綻するのでこのあたりが限界です
![image](https://github.com/user-attachments/assets/6b2c9566-9e80-48f2-b427-b7b372397449)

author_association: COLLABORATOR
user: login: Mattya3
created_at: 2025-05-29T01:09:36Z
body: 上のレビューに対してどのように対応したかそれぞれコメントいただけると助かります🙇‍♂️
author_association: COLLABORATOR
user: login: Mattya3
created_at: 2025-05-29T10:53:42Z
body: ①ユーザが作成したメモの、ボディ・タグの組を1件取得する
②llmにその組を与え、付けるべきタグリストを考えてもらう(生成タグ)
③タグリストのタグそれぞれを類似度検索し、上位1個(類似タグ)取得する
④類似度を比較し、0.9以上なら代替
⑤決定したタグのリストを登録
author_association: COLLABORATOR
user: login: ootake430
created_at: 2025-05-29T14:02:32Z
body: ## 全体の処理の流れ
1. 初期タグ（initial_tags）のセット

    - ユーザーが手動で付けたタグを、まずリストとして保持する。

2. メジャータグ抽出

    - LLMを使って、メモ本文から「アイデア」「日記」「ToDo」「イベント」などの汎用的で意味のあるタグ（メジャータグ）を抽出する。

    - すでに初期タグに入っているものは重複しないように除外。

3. アイデア区分タグの追加

    - 「アイデア」タグが初期タグまたはメジャータグに含まれていれば、さらにその「アイデア」がどのカテゴリ（例：「アプリ」「サービス」「商品」など）かLLMで判定し、その区分タグを追加。

    - 初期タグに「アイデア」がある場合は「アイデア」タグはそのまま残し、区分タグだけ追加。

    - 初期タグに無く、メジャータグ経由で「アイデア」がついた場合は「アイデア」を区分タグで置換。

4. カスタムタグ抽出

    - 本文中の人名以外の固有名詞（地名、商品名、キーワード等）をLLMで抽出し、重複しないよう追加。

5. 関連メモからのタグ提案

    - 類似する過去メモとそのタグを参考に、追加すべきタグをLLMに提案させ、重複しないものだけ追加。

6. タグリストの完成

    - 上記すべてのタグをまとめ、重複なしのタグリストとして返す。
author_association: COLLABORATOR
user: login: ootake430
created_at: 2025-05-29T14:03:06Z
body: ## 入出力サンプル

--- Sample 1 ---
new_text: みんなでシェアできる冷蔵庫アプリ作ったら便利じゃない？
initial_tags: []
related_text: 冷蔵庫の中身を家族と共有したいっていう話があった
related_tags: ['家族', '冷蔵庫', '共有']
=> generated tags: ['アプリ', 'シェア', '冷蔵庫']

--- Sample 2 ---
new_text: 早起きできた人だけ集まる朝活カフェとか面白そう。
initial_tags: ['アイデア', '朝活']
related_text: 朝活したい人向けのアプリを作る案
related_tags: ['アプリ', '朝活', '健康']
=> generated tags: ['アイデア', '朝活', 'イベント', 'カフェ']

--- Sample 3 ---
new_text: 寝る前の3分でできる瞑想ガイドの音声サービスとか需要ありそう。
initial_tags: ['音声', '健康']
related_text: 瞑想アプリで夜寝る前に使えるもの
related_tags: ['瞑想', 'アプリ', '睡眠']
=> generated tags: ['音声', '健康', '瞑想ガイド', '音声サービス']

--- Sample 4 ---
new_text: 傘のシェアリングサービス、駅に設置したらどうかな。
initial_tags: []
related_text: シェアリングエコノミーの新しいアイデアを考える
related_tags: ['シェア', 'サービス', 'エコ']
=> generated tags: ['サービス', '駅']

--- Sample 5 ---
new_text: 古着を送るとAIがコーディネート案を返してくれるサービスとかどう？
initial_tags: ['サービス']
related_text: AIでコーデ提案するアプリ考えた
related_tags: ['AI', 'アプリ', 'コーデ']
=> generated tags: ['サービス', '古着', 'コーデ']

--- Sample 6 ---
new_text: 友達の誕生日に自動でメッセージ送ってくれる機能ほしい。
initial_tags: []
related_text: 誕生日管理アプリのアイデア
related_tags: ['アプリ', '誕生日', '管理']
=> generated tags: ['サービス', '機能', '自動送信', '通知']

--- Sample 7 ---
new_text: スーパーの特売情報を毎日通知してくれるLINE bot作りたい。
initial_tags: ['LINE']
related_text: 特売情報を自動で集める仕組みが欲しい
related_tags: ['特売', '情報', '自動']
=> generated tags: ['LINE', 'サービス', '特売情報', 'LINE bot', 'スーパー', '特売', '自動']

--- Sample 8 ---
new_text: 家事をみんなで分担できるアプリがあればいいのに。
initial_tags: []
related_text: 家族で家事をシェアするためのツール
related_tags: ['家族', '家事', 'シェア']
=> generated tags: ['アプリ', '家事', 'シェア']

--- Sample 9 ---
new_text: 習慣化を支援する日記アプリ、リマインダー付きで。
initial_tags: ['日記', '習慣化']
related_text: リマインダー付きのToDoアプリ
related_tags: ['リマインダー', 'ToDo', 'アプリ']
=> generated tags: ['日記', '習慣化', 'ToDo', 'アプリ']

--- Sample 10 ---
new_text: 駅の混雑状況がリアルタイムでわかるサービスがあったら助かる。
initial_tags: []
related_text: リアルタイム情報を活用した交通アプリのアイデア
related_tags: ['リアルタイム', '交通', 'アプリ']
=> generated tags: ['サービス', '交通', 'リアルタイム']
author_association: COLLABORATOR