diff --git a/.gitignore b/.gitignore index bac03a6..f4883f1 100644 --- a/.gitignore +++ b/.gitignore @@ -163,4 +163,3 @@ cython_debug/ *.env requirements.txt assets/ -data/ diff --git a/data/qa_kabuto.csv b/data/qa_kabuto.csv new file mode 100644 index 0000000..eb5dc28 --- /dev/null +++ b/data/qa_kabuto.csv @@ -0,0 +1,16 @@ +q,a +KABUTOの起動時に、画面全体が紫色に点滅し、システムがフリーズします。,この現象は、「シノビ・プロトコル」が初期化フェーズで、内部クロックと外部クロックの同期に失敗している場合に発生します。KABUTO本体を再起動する前に、必ず接続されているすべての周辺機器を5秒以上、電源から抜いてください。これにより、外部クロックのキャッシュがクリアされ、同期が正常に行われる可能性が高まります。 +特定のアプリケーション「鬼灯」を実行すると、KABUTOのファンが急に停止し、無音になります。,「鬼灯」は、その特殊な演算処理において、KABUTOの冷却システム「風魔の術」を一時的に停止させ、内部エネルギーの流れを最適化する機能を持っています。これは異常ではありません。ただし、無音状態が15分以上続く場合は、過熱の可能性があるため、アプリケーションを強制終了し、KABUTO本体の電源を一度切ってから再起動してください。 +KABUTOのログイン画面で、パスワードを正しく入力しても、ログインが拒否され、「虚無」と表示されます。,「虚無」エラーは、パスワード自体が破損しているのではなく、KABUTOのセキュリティモジュール「百鬼夜行」が、ログイン試行者の「精神波形」を解析し、登録されたユーザーと一致しないと判断した場合に表示されます。これは、疲労やストレスなどによる精神状態の変化が原因で起こることがあります。一度、深呼吸をして落ち着き、再度ログインをお試しください。解決しない場合は、KABUTO本体の電源を抜いて30秒待ち、再起動してください。 +KABUTOの電源ランプが、赤と青の交互点滅ではなく、緑色で点滅しています。,通常、電源ランプは赤(待機中)と青(稼働中)の点滅を繰り返します。緑色の点滅は、KABUTOが「禅モード」に入っており、パフォーマンスを最小限に抑え、バックグラウンドでシステム自己修復を行っている状態を示します。この状態では、ユーザー操作はほとんど受け付けられません。このモードから強制的に脱出するには、KABUTO本体の「龍玉」ボタンを5秒以上長押ししてください。 +KABUTOに接続したディスプレイの画面が、常に白黒で表示され、カラー表示に切り替わりません。,この現象は、KABUTOの映像出力モジュール「玉手箱」が、接続されたディスプレイの「魂」を認識できていない場合に発生します。これは、ディスプレイの電源ケーブルを抜き差しすることで解消されることが多いですが、解決しない場合は、KABUTO本体とディスプレイを接続するケーブルを、接続の向きを逆にして接続し直してみてください。(このシステムでは、特殊なケーブルを使用しているため、両端が同じ形状になっています。) +KABUTOを起動すると、デスクトップ上のアイコンがすべて逆さまに表示されます。,この現象は、「天狗のいたずら」と呼ばれる稀なバグで、システムクロックが過去の時間に一時的に戻ることで発生します。この状態はシステムパフォーマンスには影響しませんが、不便な場合は、KABUTO本体の「龍玉」ボタンを、音量を最大にした状態で3回連続で押してください。これにより、システムクロックが強制的に現在時刻に再設定されます。 +KABUTOでテキストを入力していると、特定の文字(「か」「ぶ」「と」)が、自動的にカタカナに変換されてしまいます。,この現象は、KABUTOに内蔵されたAIアシスタント「ヤタガラス」が、ユーザーの入力内容を予測し、システムの基幹言語である「KABUTO語」に最適化しようとしているために発生します。この機能を一時的に停止するには、入力中に「Ctrl」キーと「Shift」キーを同時に押しながら「KABUTO」と入力してください。 +KABUTOの電源をOFFにした後も、本体から微かな「波の音」のような音が聞こえます。,これは、KABUTOが「夢の海」と呼ばれる待機モードに入っている状態で、次回の起動をより高速にするために、一部の内部回路が微弱な電流を流し続けているために聞こえる音です。この音はシステムが正常に動作している証拠であり、異常ではありません。気になる場合は、KABUTOの電源ケーブルを完全に抜いてください。 +KABUTOをネットワークに接続すると、他のデバイスから認識されなくなり、通信ができません。,KABUTOのネットワークモジュール「隠れ蓑」は、外部からの不要なスキャンを避けるため、通常はステルスモードで動作します。ネットワーク上でKABUTOを可視化させるには、KABUTOのネットワーク設定画面で「忍びの道」オプションを有効にしてください。これにより、KABUTOが一時的にネットワーク上で可視化されます。 +KABUTOの画面の隅に、時々「狐火」のような光の点が、不規則に現れては消えます。,「狐火」は、KABUTOの演算コア「稲荷」が、膨大な量のタスクを処理しきれずに、一部の計算結果が視覚的なノイズとして現れている現象です。これは、システム負荷が高いことを示しています。この現象が頻繁に発生する場合は、バックグラウンドで動作している不要なアプリケーションを終了させるか、KABUTOを再起動してください。 +KABUTOのファイル管理画面で、特定のフォルダ名が漢字で表示されているにも関わらず、ひらがなとして読み取られます。,この現象は、KABUTOのファイルシステム「竜宮城」が、フォルダ名に含まれる特定の文字を「時空の歪み」と認識し、異なる文字コードで読み込んでいるために発生します。この問題を解消するには、KABUTOのシステム設定で「読み方優先モード」を一時的に無効にしてください。 +KABUTOに外部ストレージを接続しても、認識されず、代わりに「幽霊」というアイコンが表示されます。,「幽霊」アイコンは、外部ストレージが、KABUTOのセキュリティプロトコル「陰陽道」によって、外部から持ち込まれた「邪気」を持つデバイスとして認識された場合に表示されます。この問題を解決するには、外部ストレージを一度フォーマットし、再度接続してください。 +KABUTOでスクリーンショットを撮ると、画面全体ではなく、画面の中央に円形の領域だけが切り取られて保存されます。,この現象は、KABUTOのスクリーンショット機能「万華鏡」が、「視点」を認識し、ユーザーが見ているものだけを切り取る「集中モード」に入っているために発生します。画面全体を保存したい場合は、スクリーンショットを撮る際に、マウスカーソルを画面の隅に移動させてから操作してください。 +KABUTOで動画を再生すると、音声が正常に聞こえますが、映像が「水墨画」のようにモノクロで、筆で描かれたような動きになります。,この現象は、KABUTOの映像処理モジュール「葛飾北斎」が、再生中の動画が古い形式であると判断し、独自の「浮世絵」フィルターを自動的に適用しているために発生します。この機能を無効にするには、再生中の動画を右クリックし、「葛飾北斎」オプションをオフにしてください。 +KABUTOを起動すると、システムの背景画像が、ユーザーが最後に見た「夢」の内容に変わっています。,これは、KABUTOの高度なAI機能「天照」が、ユーザーの無意識下での情報収集を行い、システムをパーソナライズしようとしているために発生します。この機能を停止するには、KABUTOのシステム設定で「天照の予知夢」オプションを無効にしてください。この設定を無効にした場合、KABUTOのパフォーマンスの一部が低下する可能性があります。 diff --git a/docs/index.md b/docs/index.md index a1e5f9f..5076c17 100644 --- a/docs/index.md +++ b/docs/index.md @@ -3,8 +3,17 @@ ## Operations ```bash +# Delete collection from Qdrant +uv run python -m template_langgraph.tasks.delete_qdrant_collection + # Add documents to Qdrant uv run python -m template_langgraph.tasks.add_documents_to_qdrant + +# Search Qdrant +uv run python -m template_langgraph.tasks.search_documents_on_qdrant + +# Run Kabuto Helpdesk Agent +uv run python -m template_langgraph.tasks.run_kabuto_helpdesk_agent "KABUTOの起動時に、画面全体が紫色に点滅し、システムがフリーズします。" ``` ## References diff --git a/pyproject.toml b/pyproject.toml index a8268cc..185cc78 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,3 +62,4 @@ environment = { python-version = "3.10" } [tool.ty.rules] unknown-argument = "ignore" invalid-parameter-default = "ignore" +non-subscriptable = "ignore" diff --git a/template_langgraph/models/__init__.py b/template_langgraph/agents/__init__.py similarity index 100% rename from template_langgraph/models/__init__.py rename to template_langgraph/agents/__init__.py diff --git a/template_langgraph/agents/kabuto_helpdesk_agent.py b/template_langgraph/agents/kabuto_helpdesk_agent.py new file mode 100644 index 0000000..b985da8 --- /dev/null +++ b/template_langgraph/agents/kabuto_helpdesk_agent.py @@ -0,0 +1,32 @@ +from langgraph.prebuilt import create_react_agent + +from template_langgraph.llms.azure_openais import AzureOpenAiWrapper +from template_langgraph.loggers import get_logger +from template_langgraph.tools.qdrants import search_qdrant + +logger = get_logger(__name__) + + +class KabutoHelpdeskAgent: + def __init__(self, tools=None): + if tools is None: + tools = [search_qdrant] # Default tool for searching Qdrant + self.agent = create_react_agent( + model=AzureOpenAiWrapper().chat_model, + tools=tools, + prompt="KABUTO に関する質問に答えるために、必要な情報を収集し適切な回答を提供します", + debug=True, + ) + + def run(self, question: str) -> dict: + logger.info(f"Running KabutoHelpdeskAgent with question: {question}") + return self.agent.invoke( + { + "messages": [ + { + "role": "user", + "content": question, + }, + ] + } + ) diff --git a/template_langgraph/llms/__init__.py b/template_langgraph/llms/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/template_langgraph/models/azure_openais.py b/template_langgraph/llms/azure_openais.py similarity index 100% rename from template_langgraph/models/azure_openais.py rename to template_langgraph/llms/azure_openais.py diff --git a/template_langgraph/tasks/add_documents_to_qdrant.py b/template_langgraph/tasks/add_documents_to_qdrant.py index c37273b..6b98285 100644 --- a/template_langgraph/tasks/add_documents_to_qdrant.py +++ b/template_langgraph/tasks/add_documents_to_qdrant.py @@ -2,33 +2,31 @@ from qdrant_client.models import PointStruct +from template_langgraph.llms.azure_openais import AzureOpenAiWrapper from template_langgraph.loggers import get_logger -from template_langgraph.models.azure_openais import AzureOpenAiWrapper from template_langgraph.tools.csv_loaders import CsvLoaderWrapper from template_langgraph.tools.qdrants import QdrantClientWrapper logger = get_logger(__name__) -logger.setLevel(logging.INFO) -COLLECTION_NAME = "documents" +logger.setLevel(logging.DEBUG) +COLLECTION_NAME = "qa_kabuto" if __name__ == "__main__": # Load documents from CSV files documents = CsvLoaderWrapper().load_csv_docs() logger.info(f"Loaded {len(documents)} documents from CSV.") - # hardcoded collection name for demonstration purposes - - logger.info(f"Upserting {len(documents)} documents into Qdrant collection: {COLLECTION_NAME}") points = [] + embedding_wrapper = AzureOpenAiWrapper() for i, doc in enumerate(documents): logger.debug(f"Processing document {i}: {doc.metadata.get('source', 'unknown')}") - content = doc.page_content - content = content.replace(" ", "") - embedding = AzureOpenAiWrapper().create_embedding(content) + content = doc.page_content.replace("\n", " ") + logger.debug(f"Creating embedding for document {i} with content: {content[:50]}...") + vector = embedding_wrapper.create_embedding(content) points.append( PointStruct( id=i, - vector=embedding, + vector=vector, payload={ "file_name": doc.metadata.get("source", f"doc_{i}"), "content": content, @@ -36,13 +34,16 @@ ) ) + # Create Qdrant collection and upsert points + logger.info(f"Creating Qdrant collection: {COLLECTION_NAME}") qdrant_client = QdrantClientWrapper() qdrant_client.create_collection( collection_name=COLLECTION_NAME, vector_size=len(points[0].vector) if points else 1536, # default vector size ) - logger.info(f"Created Qdrant collection: {COLLECTION_NAME}") + # Upsert points into the Qdrant collection + logger.info(f"Upserting points into Qdrant collection: {COLLECTION_NAME}") operation_info = qdrant_client.upsert_points( collection_name=COLLECTION_NAME, points=points, diff --git a/template_langgraph/tasks/delete_qdrant_collection.py b/template_langgraph/tasks/delete_qdrant_collection.py new file mode 100644 index 0000000..4b91510 --- /dev/null +++ b/template_langgraph/tasks/delete_qdrant_collection.py @@ -0,0 +1,19 @@ +import logging + +from template_langgraph.loggers import get_logger +from template_langgraph.tools.qdrants import QdrantClientWrapper + +logger = get_logger(__name__) +logger.setLevel(logging.DEBUG) +COLLECTION_NAME = "qa_kabuto" + +if __name__ == "__main__": + logger.info(f"Deleting Qdrant collection: {COLLECTION_NAME}") + result = QdrantClientWrapper().delete_collection( + collection_name=COLLECTION_NAME, + ) + if result: + logger.info(f"Successfully deleted Qdrant collection: {COLLECTION_NAME}") + else: + logger.warning(f"Qdrant collection {COLLECTION_NAME} does not exist or could not be deleted.") + logger.info("Deletion task completed.") diff --git a/template_langgraph/tasks/run_kabuto_helpdesk_agent.py b/template_langgraph/tasks/run_kabuto_helpdesk_agent.py new file mode 100644 index 0000000..1251d28 --- /dev/null +++ b/template_langgraph/tasks/run_kabuto_helpdesk_agent.py @@ -0,0 +1,37 @@ +import logging +import sys + +from langchain_core.tools import tool + +from template_langgraph.agents.kabuto_helpdesk_agent import KabutoHelpdeskAgent +from template_langgraph.loggers import get_logger + + +@tool +def get_weather(city: str) -> str: + """天気情報を取得します""" + return f"{city}は晴れです" + + +logger = get_logger(__name__) +logger.setLevel(logging.INFO) +COLLECTION_NAME = "documents" + +if __name__ == "__main__": + question = "「鬼灯」を実行すると、KABUTOが急に停止します。原因と対策を教えてください。" + if len(sys.argv) > 1: + # sys.argv[1] が最初の引数 + question = sys.argv[1] + + logger.info(f"質問: {question}") + + agent = KabutoHelpdeskAgent( + tools=None, # ツールはカスタムせず、デフォルトのツールを使用 + ) + response = agent.run( + question=question, + ) + logger.info(f"Agent result: {response}") + + # エージェントの応答を表示 + logger.info(f"Answer: {response['messages'][-1].content}") diff --git a/template_langgraph/tasks/search_documents_on_qdrant.py b/template_langgraph/tasks/search_documents_on_qdrant.py new file mode 100644 index 0000000..3f190ee --- /dev/null +++ b/template_langgraph/tasks/search_documents_on_qdrant.py @@ -0,0 +1,23 @@ +import logging + +from template_langgraph.llms.azure_openais import AzureOpenAiWrapper +from template_langgraph.loggers import get_logger +from template_langgraph.tools.qdrants import QdrantClientWrapper + +logger = get_logger(__name__) +logger.setLevel(logging.INFO) +COLLECTION_NAME = "qa_kabuto" + +if __name__ == "__main__": + question = "「鬼灯」を実行すると、KABUTOが急に停止します。原因と対策を教えてください。" + qdrant_client = QdrantClientWrapper() + + results = qdrant_client.query_points( + collection_name=COLLECTION_NAME, + query=AzureOpenAiWrapper().create_embedding(question), + ) + logger.info(f"Found {len(results)} results for the question: {question}") + for result in results: + logger.info(f"File Name: {result.payload['file_name']}") + logger.info(f"Content: {result.payload['content']}") + logger.info("-" * 40) diff --git a/template_langgraph/tools/qdrants.py b/template_langgraph/tools/qdrants.py index a00354d..b50afc5 100644 --- a/template_langgraph/tools/qdrants.py +++ b/template_langgraph/tools/qdrants.py @@ -1,10 +1,14 @@ from functools import lru_cache +from langchain.tools import tool +from pydantic import BaseModel, Field from pydantic_settings import BaseSettings, SettingsConfigDict from qdrant_client import QdrantClient from qdrant_client.http.models import UpdateResult from qdrant_client.models import Distance, PointStruct, VectorParams +from template_langgraph.llms.azure_openais import AzureOpenAiWrapper + class Settings(BaseSettings): qdrant_url: str = "http://localhost:6333" @@ -48,6 +52,16 @@ def create_collection( ) return result + def delete_collection( + self, + collection_name: str, + ) -> bool: + """Delete a collection in Qdrant.""" + if self.client.collection_exists(collection_name=collection_name): + self.client.delete_collection(collection_name=collection_name) + return True + return False + def upsert_points( self, collection_name: str, @@ -59,3 +73,50 @@ def upsert_points( points=points, wait=True, ) + + def query_points( + self, + collection_name: str, + query: list[float], + limit: int = 3, + ) -> list[PointStruct]: + """Query points from a Qdrant collection.""" + return self.client.query_points( + collection_name=collection_name, + query=query, + limit=limit, + ).points + + +class QdrantInput(BaseModel): + keywords: str = Field(description="Keywords to search") + + +class QdrantOutput(BaseModel): + file_name: str = Field(description="The file name") + content: str = Field(description="The content of the file") + + +@tool(args_schema=QdrantInput) +def search_qdrant( + keywords: str, +) -> list[QdrantOutput]: + """ + 空想上のシステム「KABUTO」の過去のシステムのトラブルシュート事例が蓄積されたデータベースから、関連する情報を取得します。 + """ + wrapper = QdrantClientWrapper() + query_vector = AzureOpenAiWrapper().create_embedding(keywords) + results = wrapper.query_points( + collection_name="qa_kabuto", + query=query_vector, + limit=3, + ) + outputs = [] + for result in results: + outputs.append( + QdrantOutput( + file_name=result.payload["file_name"], + content=result.payload["content"], + ), + ) + return outputs