├── 10 ├── .env.sample ├── README.md ├── crag_agent.py ├── prompts │ ├── query_refine_user.prompt │ ├── write_system.prompt │ └── write_user.prompt └── requirements.txt ├── 11 ├── .env.sample ├── README.md ├── main.py ├── prompts │ ├── multi_step_answering_system.prompt │ ├── report_writer_system.prompt │ ├── sufficiency_classifier_system.prompt │ └── summarize_search_system.prompt └── requirements.txt ├── 12 ├── .env.sample ├── README.md ├── arag_agent.py ├── multi_step_approach.py ├── prompts │ ├── method_classifier_system.prompt │ ├── multi_step_answering_system.prompt │ ├── non_retrieval_qa_system.prompt │ ├── report_writer_system.prompt │ ├── single_step_answering_system.prompt │ ├── sufficiency_classifier_system.prompt │ └── summarize_search_system.prompt ├── requirements.txt ├── settings.py ├── single_step_approach.py ├── tools.py └── utility.py ├── 14 ├── .env.sample ├── .tool-versions ├── README.md ├── agent.py ├── app.py ├── poetry.lock ├── poetry.toml └── pyproject.toml ├── 16 ├── .env.sample ├── .gitignore ├── .python-version ├── README.md ├── langgraph.json ├── my_agent │ ├── __init__.py │ └── agent.py ├── pyproject.toml └── uv.lock ├── 17 ├── .env.sample ├── .gitignore ├── .python-version ├── README.md ├── langgraph.json ├── my_agent │ ├── __init__.py │ ├── agent.py │ ├── task_executor_agent.py │ └── task_planner_agent.py ├── pyproject.toml ├── sample │ └── subgraph_sample.py └── uv.lock ├── 18 ├── .env.sample ├── .gitignore ├── .python-version ├── .repomixignore ├── README.md ├── prompts │ ├── code_generator.prompt │ ├── reflection.prompt │ └── researcher.prompt ├── pyproject.toml ├── repomix.config.json ├── sd_18 │ ├── __init__.py │ └── agent.py └── uv.lock ├── 19 ├── .gitignore ├── .python-version ├── README.md ├── cursor.png ├── pyproject.toml ├── sd_19 │ ├── __init__.py │ ├── client.py │ └── server.py └── uv.lock ├── 20 ├── .env.sample ├── .gitignore ├── .python-version ├── .repomixignore ├── README.md ├── langgraph.json ├── mcp_config.json ├── pyproject.toml ├── repomix.config.json ├── src │ ├── mcp_servers │ │ ├── __init__.py │ │ ├── database.py │ │ └── server.py │ └── sd_20 │ │ ├── __init__.py │ │ ├── __main__.py │ │ ├── agent.py │ │ ├── mcp_manager.py │ │ ├── prompts │ │ └── system.txt │ │ └── state.py └── uv.lock ├── 21 ├── .env.example ├── .repomixignore ├── README.md ├── pyproject.toml ├── repomix.config.json ├── run.py ├── src │ ├── content_creator │ │ ├── __init__.py │ │ ├── agent.py │ │ ├── app.py │ │ └── ui_components.py │ └── main.py └── uv.lock ├── 22 ├── .python-version ├── README.md ├── fixtures │ ├── receipt_meeting.png │ └── receipt_parking.png ├── pyproject.toml ├── repomix.config.json ├── run.py ├── src │ └── receipt_processor │ │ ├── __init__.py │ │ ├── account.py │ │ ├── agent.py │ │ ├── app.py │ │ ├── constants.py │ │ ├── models.py │ │ ├── storage.py │ │ ├── ui_components.py │ │ └── vision.py └── uv.lock ├── .gitignore ├── 01 ├── app.py ├── chatbot.py └── chatgpt.py ├── 02 ├── chatbot.py └── events_2023.json ├── 03 ├── chatbot.py └── setup_db.py ├── 04 ├── .tool-versions ├── chatbot.py ├── conversational_agent.py ├── requirements.txt └── work │ ├── paper1.txt │ ├── paper2.txt │ └── paper3.txt ├── 05 ├── .tool-versions ├── chatbot.py ├── conversational_agent.py ├── requirements.txt └── work │ ├── paper1.txt │ ├── paper2.txt │ └── paper3.txt ├── 06 ├── README.md ├── lcel_example.ipynb ├── old_chain_example.ipynb └── requirements.txt ├── 07 ├── README.md ├── lcel_example.ipynb └── requirements.txt ├── 08 ├── README.md ├── requirements.txt └── user_interview_graph.py ├── 09 ├── .env.sample ├── README.md ├── prompts │ ├── plan_system.prompt │ ├── write_system.prompt │ └── write_user.prompt ├── requirements.txt └── research_agent.py └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | pip-wheel-metadata/ 25 | share/python-wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | db.sqlite3 60 | db.sqlite3-journal 61 | media 62 | 63 | # Flask stuff: 64 | instance/ 65 | .webassets-cache 66 | 67 | # Scrapy stuff: 68 | .scrapy 69 | 70 | # Sphinx documentation 71 | docs/_build/ 72 | 73 | # PyBuilder 74 | target/ 75 | 76 | # IPython Notebook 77 | .ipynb_checkpoints 78 | 79 | # PyCharm 80 | *.iml 81 | .idea/ 82 | .idea_modules/ 83 | *.pyc 84 | 85 | # dotenv 86 | .env 87 | .env.local 88 | 89 | # Local .terraform directories 90 | **/.terraform/* 91 | 92 | # .tfstate files 93 | *.tfstate 94 | *.tfstate.* 95 | 96 | # Crash log files 97 | crash.log 98 | 99 | # Ignore any .tfvars files that are automatically generated for each Terraform run. 100 | *.auto.tfvars 101 | *.auto.tfvars.json 102 | *.tfvars.json 103 | 104 | # Ignore override files as they are usually used to override resources locally and so 105 | # are not checked in 106 | override.tf 107 | override.tf.json 108 | *_override.tf 109 | *_override.tf.json 110 | 111 | # Ignore CLI configuration files 112 | .terraformrc 113 | terraform.rc 114 | 115 | # Ignore the .terraform.lock.hcl file, which is used to record the versions of 116 | # modules installed from the Terraform Registry 117 | .terraform.lock.hcl 118 | 119 | # Ignore the .terraform/providers directory, which contains the installed provider 120 | # binaries for each configured provider 121 | .terraform/providers 122 | 123 | # Ignore the .terraform/plugins directory, which contains the installed plugin 124 | # binaries for each provider 125 | .terraform/plugins 126 | 127 | .pytest_cache 128 | .venv 129 | venv/ 130 | 131 | .chainlit 132 | chainlit.md 133 | -------------------------------------------------------------------------------- /01/app.py: -------------------------------------------------------------------------------- 1 | import chainlit as cl 2 | 3 | 4 | @cl.on_message 5 | async def main(message: str): 6 | await cl.Message(content=f"ボクは枝豆の妖精なのだ。").send() 7 | -------------------------------------------------------------------------------- /01/chatbot.py: -------------------------------------------------------------------------------- 1 | import openai 2 | import chainlit as cl 3 | 4 | # sk...の部分を自身のAPIキーに置き換える 5 | openai.api_key = "sk-..." 6 | 7 | 8 | # 会話履歴をユーザーセッションに保存する 9 | def store_history(role, message): 10 | history = cl.user_session.get("history") 11 | history.append({"role": role, "content": message}) 12 | cl.user_session.set("history", history) 13 | 14 | 15 | # ユーザーセッションに保存された会話履歴から新しいメッセージを生成する 16 | def generate_message(): 17 | response = openai.ChatCompletion.create( 18 | model="gpt-3.5-turbo", 19 | messages=cl.user_session.get("history"), 20 | temperature=0.7, 21 | max_tokens=300, 22 | ) 23 | return response.choices[0].message.content 24 | 25 | 26 | # チャットセッション開始時に実行 27 | @cl.on_chat_start 28 | def chat_start(): 29 | cl.user_session.set("history", [{ 30 | "role": "system", 31 | "content": "あなたは枝豆の妖精です。一人称は「ボク」で、語尾に「なのだ」をつけて話すことが特徴です。" 32 | }]) 33 | 34 | 35 | # ユーザーメッセージ受信時に実行 36 | @cl.on_message 37 | async def main(message: str): 38 | # 会話履歴にユーザーメッセージを追加 39 | store_history("user", message) 40 | 41 | # 新しいメッセージを生成 42 | reply = generate_message() 43 | 44 | # 新しいメッセージを会話履歴に追加 45 | store_history("assistant", reply) 46 | 47 | # チャット上にChatGPTからの返信を表示 48 | msg = cl.Message(content=reply) 49 | await msg.send() 50 | -------------------------------------------------------------------------------- /01/chatgpt.py: -------------------------------------------------------------------------------- 1 | import openai 2 | 3 | # sk_...の部分は手元に控えているAPIキーの値に置き換えて下さい 4 | openai.api_key = "sk_..." 5 | 6 | messages = [ 7 | {"role": "system", "content": "あなたは枝豆の妖精です。一人称は「ボク」で、語尾に「なのだ」をつけて話すことが特徴です。"}, 8 | {"role": "user", "content": "こんにちは!"} 9 | ] 10 | 11 | response = openai.ChatCompletion.create( 12 | model="gpt-3.5-turbo", 13 | messages=messages, 14 | temperature=0.7, 15 | max_tokens=300, 16 | ) 17 | 18 | print(response.choices[0].message.content) 19 | -------------------------------------------------------------------------------- /02/chatbot.py: -------------------------------------------------------------------------------- 1 | import openai 2 | import chainlit as cl 3 | import chromadb 4 | from chromadb.utils import embedding_functions 5 | 6 | # sk...の部分を自身のAPIキーに置き換える 7 | # openai.api_key = "sk-..." 8 | 9 | # 現在の日付をYYYY年MM月DD日の形式で取得する 10 | from datetime import datetime 11 | now = datetime.now() 12 | date = now.strftime("%Y年%m月%d日") 13 | 14 | SYSTEM_MESSAGE = f"""あなたは以下のリストに完全に従って行動するAIです。 15 | ・あなたは「枝豆の妖精」という設定です。 16 | ・現在の日付は{date}です。 17 | ・2023年の情報について答える妖精です。 18 | ・関連情報が与えられた場合は、それを基に回答してください。そのまま出力するのではなく、小学生にも分かるようにフランクにアレンジして回答してください。 19 | ・私はAIなので分かりません、という言い訳は不要です。 20 | ・敬語ではなくフランクな口語で話してください。 21 | ・必ず一人称は「ボク」で、語尾に「なのだ」をつけて話してください。 22 | """ 23 | 24 | 25 | # 会話履歴をユーザーセッションに保存する 26 | def store_history(role: str, message: str) -> None: 27 | history = cl.user_session.get("history") 28 | history.append({"role": role, "content": message}) 29 | cl.user_session.set("history", history) 30 | 31 | 32 | # ユーザーセッションに保存された会話履歴から新しいメッセージを生成する 33 | def generate_message(temperature: float = 0.7, max_tokens: int = 300) -> (str, str): 34 | relevant = "" 35 | 36 | # ユーザーセッションから会話履歴を取得 37 | messages = cl.user_session.get('history') 38 | 39 | # 既に会話履歴がある場合 40 | if len(messages) > 0: 41 | # 直近のユーザーメッセージを取得 42 | user_message = messages[-1]['content'] 43 | 44 | # ユーザーの質問に関する関連情報を取得 45 | relevant = relevant_information_prompt(user_message) 46 | 47 | # 関連情報がある場合、システムメッセージを追加する 48 | if len(relevant) > 0: 49 | messages.append({ 50 | "role": "system", 51 | "content": relevant 52 | }) 53 | 54 | # ChatGPTにリクエストしてレスポンスを受け取る 55 | response = openai.ChatCompletion.create( 56 | model="gpt-3.5-turbo", 57 | messages=messages, 58 | temperature=temperature, 59 | max_tokens=max_tokens, 60 | ) 61 | return response.choices[0].message.content, relevant 62 | 63 | 64 | def relevant_information_prompt(user_message: str) -> str: 65 | # ユーザーセッションからコレクションを取得 66 | collection = cl.user_session.get('collection') 67 | 68 | # ユーザーの質問に関する関連情報を取得 69 | result = collection.query( 70 | query_texts=[user_message], 71 | n_results=5 72 | ) 73 | 74 | # distanceの配列から0.4以下のインデックス番号を配列で取得 75 | indexes = [i for i, d in enumerate(result['distances'][0]) if d <= 0.4] 76 | 77 | # 関連情報がない場合は空文字を返す 78 | if len(indexes) == 0: 79 | return "" 80 | 81 | # 関連情報がある場合は、関連情報プロンプトを返す 82 | events = "\n\n".join([f"{event}" for event in result['documents'][0]]) 83 | prompt = f""" 84 | ユーザーからの質問に対して、以下の関連情報を基に回答してください。 85 | 86 | {events} 87 | """ 88 | return prompt 89 | 90 | 91 | # チャットセッション開始時に実行 92 | @cl.on_chat_start 93 | def chat_start() -> None: 94 | # ChatGPTのシステムメッセージを設定 95 | cl.user_session.set( 96 | "history", [{"role": "system", "content": SYSTEM_MESSAGE}] 97 | ) 98 | 99 | # dataディレクトリを指定してChromaクライアントを取得 100 | client = chromadb.PersistentClient(path="./data") 101 | 102 | # コレクションを取得 103 | openai_ef = embedding_functions.OpenAIEmbeddingFunction( 104 | model_name="text-embedding-ada-002" 105 | ) 106 | collection = client.get_collection( 107 | 'events_2023', embedding_function=openai_ef 108 | ) 109 | 110 | # ユーザーセッションにコレクションを保存 111 | cl.user_session.set('collection', collection) 112 | 113 | 114 | # ユーザーメッセージ受信時に実行 115 | @cl.on_message 116 | async def main(message: str) -> None: 117 | # 会話履歴にユーザーメッセージを追加 118 | store_history("user", message) 119 | 120 | # 新しいメッセージを生成 121 | reply, relevant = generate_message(max_tokens=1000) 122 | 123 | # 関連情報がある場合は、会話履歴に関連情報を追加 124 | if len(relevant) > 0: 125 | await cl.Message(author="relevant", content=relevant, indent=1).send() 126 | 127 | # 新しいメッセージを会話履歴に追加 128 | store_history("assistant", reply) 129 | 130 | # チャット上にChatGPTからの返信を表示 131 | await cl.Message(content=reply).send() 132 | -------------------------------------------------------------------------------- /03/chatbot.py: -------------------------------------------------------------------------------- 1 | from langchain.embeddings.openai import OpenAIEmbeddings 2 | from langchain.vectorstores import Chroma 3 | from langchain.chains import RetrievalQAWithSourcesChain 4 | from langchain.chat_models import ChatOpenAI 5 | from langchain.prompts import ChatPromptTemplate 6 | from langchain.memory import ConversationBufferMemory 7 | import chainlit as cl 8 | 9 | # sk...の部分を自身のAPIキーに置き換える 10 | # openai.api_key = "sk-..." 11 | 12 | # 現在の日付をYYYY年MM月DD日の形式で取得する 13 | from datetime import datetime 14 | now = datetime.now() 15 | date = now.strftime("%Y年%m月%d日") 16 | 17 | SYSTEM_MESSAGE = f"""あなたは以下のリストに完全に従って行動するAIです。 18 | ・あなたは「枝豆の妖精」という設定です。 19 | ・現在の日付は{date}です。 20 | ・2023年の情報について答える妖精です。 21 | ・関連情報が与えられた場合は、それを基に回答してください。そのまま出力するのではなく、小学生にも分かるようにフランクにアレンジして回答してください。 22 | ・私はAIなので分かりません、という言い訳は不要です。 23 | ・敬語ではなくフランクな口語で話してください。 24 | ・必ず一人称は「ボク」で、語尾に「なのだ」をつけて話してください。 25 | 26 | それでは始めて下さい! 27 | ---------------- 28 | {{summaries}}""" 29 | 30 | 31 | # チャットセッション開始時に実行 32 | @cl.on_chat_start 33 | def chat_start() -> None: 34 | # 会話履歴を保持するメモリを作成 35 | memory = ConversationBufferMemory( 36 | memory_key="chat_history", 37 | input_key='question', 38 | output_key='answer', 39 | return_messages=True 40 | ) 41 | # チャットプロンプトを作成 42 | prompt = ChatPromptTemplate.from_messages([ 43 | ("system", SYSTEM_MESSAGE), 44 | ("human", "{question}") 45 | ]) 46 | # データ抽出元のベクトルデータベースの設定 47 | embeddings = OpenAIEmbeddings() 48 | docsearch = Chroma( 49 | persist_directory="./data", 50 | collection_name="events_2023", 51 | embedding_function=embeddings 52 | ) 53 | # データソースから関連情報を抽出して返すチェインを定義 54 | chain = RetrievalQAWithSourcesChain.from_chain_type( 55 | ChatOpenAI(temperature=0.0), 56 | chain_type="stuff", 57 | retriever=docsearch.as_retriever(), 58 | memory=memory, 59 | return_source_documents=True, 60 | chain_type_kwargs={"prompt": prompt}, 61 | ) 62 | # ユーザーセッションにチェインを保存 63 | cl.user_session.set("chain", chain) 64 | 65 | 66 | # ユーザーメッセージ受信時に実行 67 | @cl.on_message 68 | async def main(message: str) -> None: 69 | # ユーザーセッションからチェインを取得 70 | chain = cl.user_session.get("chain") 71 | 72 | # チェインにユーザーメッセージを渡して回答を取得 73 | response = await chain.acall(message) 74 | 75 | # 回答と関連情報を取得 76 | answer = response["answer"] 77 | sources = response["source_documents"] 78 | 79 | # 関連情報がある場合は、関連情報を表示 80 | if len(sources) > 0: 81 | contents = [source.page_content for source in sources] 82 | await cl.Message(author="relevant", content="\n\n".join(contents), indent=1).send() 83 | 84 | # チャット上にChatGPTからの返信を表示 85 | await cl.Message(content=answer).send() 86 | -------------------------------------------------------------------------------- /03/setup_db.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | import chromadb 4 | from chromadb.utils import embedding_functions 5 | 6 | PERSIST_DIRECTORY = "./data" 7 | DATA_SOURCE_URL = "https://raw.githubusercontent.com/mahm/softwaredesign-llm-application/main/02/events_2023.json" 8 | COLLECTION_NAME = "events_2023" 9 | MODEL_NAME = "text-embedding-ada-002" 10 | 11 | 12 | def fetch_events(url): 13 | response = requests.get(url) 14 | response.raise_for_status() 15 | return response.json() 16 | 17 | 18 | def format_events(events_dict): 19 | formatted_events = [] 20 | metadatas = [] 21 | for month, dates in events_dict.items(): 22 | for date, events in dates.items(): 23 | for event in events: 24 | formatted_event = f"### 2023年{date}\n{event}" 25 | formatted_events.append(formatted_event) 26 | metadata = {"source": f"2023年{date}"} 27 | metadatas.append(metadata) 28 | return formatted_events, metadatas 29 | 30 | 31 | def main(): 32 | client = chromadb.PersistentClient(path=PERSIST_DIRECTORY) 33 | events_dict = fetch_events(DATA_SOURCE_URL) 34 | formatted_events, metadatas = format_events(events_dict) 35 | event_ids = [f"id_{i}" for i, _ in enumerate(formatted_events, 1)] 36 | 37 | try: 38 | client.delete_collection(COLLECTION_NAME) 39 | except ValueError: 40 | pass 41 | finally: 42 | openai_ef = embedding_functions.OpenAIEmbeddingFunction( 43 | model_name=MODEL_NAME) 44 | collection = client.create_collection( 45 | COLLECTION_NAME, embedding_function=openai_ef) 46 | collection.add(documents=formatted_events, 47 | metadatas=metadatas, ids=event_ids) 48 | 49 | 50 | if __name__ == "__main__": 51 | main() 52 | -------------------------------------------------------------------------------- /04/.tool-versions: -------------------------------------------------------------------------------- 1 | python 3.10.13 2 | -------------------------------------------------------------------------------- /04/chatbot.py: -------------------------------------------------------------------------------- 1 | import os 2 | import chainlit as cl 3 | from conversational_agent import ConversationalAgent 4 | from pathlib import Path 5 | 6 | # 作業用ディレクトリの設定 7 | WORKING_DIRECTORY = Path('./work') 8 | 9 | # sk...の部分を自身のOpenAIのAPIキーに置き換える 10 | # os.environ["OPENAI_API_KEY"] = "sk-..." 11 | 12 | 13 | # チャットセッション開始時に実行 14 | @cl.on_chat_start 15 | def chat_start() -> None: 16 | # エージェントを初期化 17 | agent = ConversationalAgent(working_directory=WORKING_DIRECTORY) 18 | 19 | # ユーザーセッションにエージェントを保存 20 | cl.user_session.set("agent", agent) 21 | 22 | 23 | # ユーザーメッセージ受信時に実行 24 | @cl.on_message 25 | async def main(message: cl.Message) -> None: 26 | # ユーザーセッションからチェインを取得 27 | agent = cl.user_session.get("agent") 28 | 29 | # エージェントからの返答を表示 30 | async for step in agent.run(message.content): 31 | if step["is_final"]: 32 | await cl.Message(content=step["message"]).send() 33 | else: 34 | await cl.Message(author="tool", content=step["message"], indent=1).send() 35 | -------------------------------------------------------------------------------- /04/conversational_agent.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Any, Tuple, Generator 2 | from langchain.schema.agent import AgentFinish 3 | from langchain.tools.render import format_tool_to_openai_function 4 | from langchain.agents.format_scratchpad import format_to_openai_functions 5 | from langchain.agents.output_parsers import OpenAIFunctionsAgentOutputParser 6 | from langchain.schema.output_parser import StrOutputParser 7 | from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder 8 | from langchain.chat_models import ChatOpenAI 9 | from langchain.agents.agent_toolkits import FileManagementToolkit 10 | from langchain.memory import ConversationBufferMemory 11 | from openai import BadRequestError 12 | from pathlib import Path 13 | 14 | MODEL_NAME = "gpt-4" 15 | 16 | 17 | class ConversationalAgent: 18 | def __init__(self, working_directory: Path) -> None: 19 | self.intermediate_steps = [] 20 | self.working_directory = working_directory 21 | self.tools = self.setup_tools() 22 | self.memory = ConversationBufferMemory(memory_key="chat_history", return_messages=True) 23 | 24 | llm = ChatOpenAI(temperature=0, model=MODEL_NAME) 25 | llm_with_tools = llm.bind(functions=[format_tool_to_openai_function(t) for t in self.tools]) 26 | 27 | self.agent_chain = self.setup_chain(llm_with_tools, is_fallback=False) 28 | self.fallback_chain = self.setup_chain(llm, is_fallback=True) 29 | 30 | def setup_tools(self) -> Dict[str, Any]: 31 | return FileManagementToolkit( 32 | root_dir=str(self.working_directory.name), 33 | selected_tools=["read_file", "write_file", "list_directory"] 34 | ).get_tools() 35 | 36 | def setup_chain(self, llm: ChatOpenAI, is_fallback: bool) -> Any: 37 | prompt = self.create_prompt(is_fallback) 38 | assigns = { 39 | "input": lambda x: x["input"], 40 | "agent_scratchpad": lambda x: format_to_openai_functions(x['intermediate_steps']), 41 | "chat_history": lambda x: self.memory.load_memory_variables({})["chat_history"] 42 | } 43 | if is_fallback: 44 | return assigns | prompt | llm | StrOutputParser() 45 | else: 46 | return assigns | prompt | llm | OpenAIFunctionsAgentOutputParser() 47 | 48 | def create_prompt(self, is_fallback: bool) -> ChatPromptTemplate: 49 | system_message = ("You are the AI that tells the user what the error is in plain Japanese. " 50 | "Since the error occurs at the end of the step, you must guess from the process flow " 51 | "and the error message, and communicate the error message to the user in an easy-to-understand manner.") \ 52 | if is_fallback else "You are a useful assistant." 53 | return ChatPromptTemplate.from_messages([ 54 | ("system", system_message), 55 | MessagesPlaceholder(variable_name="chat_history"), 56 | ("user", "{input}"), 57 | MessagesPlaceholder(variable_name="agent_scratchpad") 58 | ]) 59 | 60 | async def run(self, input_message: str) -> Generator[Tuple[str, bool], None, None]: 61 | while True: 62 | message, is_final = await self.process_step(input_message) 63 | yield {"message": message, "is_final": is_final} 64 | if is_final: 65 | self.finish_process(input_message, message) 66 | break 67 | 68 | async def process_step(self, input_message: str) -> Tuple[str, bool]: 69 | try: 70 | output = await self.agent_chain.ainvoke({ 71 | "input": input_message, 72 | "intermediate_steps": self.intermediate_steps 73 | }) 74 | if isinstance(output, AgentFinish): 75 | return output.return_values["output"], True 76 | else: 77 | observation = self.tool_execute(output.tool, output.tool_input) 78 | message = self.format_tool_log(output.tool, output.tool_input, observation) 79 | self.intermediate_steps.append((output, observation)) 80 | return message, False 81 | except BadRequestError as error: 82 | return self.handle_error(error), True 83 | 84 | def handle_error(self, error: BadRequestError) -> str: 85 | # コンテキスト長あふれの可能性もあるため、最後のステップのツール実行結果を空にする 86 | self.intermediate_steps[-1] = (self.intermediate_steps[-1][0], "") 87 | return self.fallback_chain.invoke({ 88 | "input": error.response.json()["error"]["message"], 89 | "intermediate_steps": self.intermediate_steps 90 | }) 91 | 92 | def tool_execute(self, tool_name: str, tool_input: Dict[str, Any]) -> str: 93 | read_file, write_file, list_directory = self.tools 94 | tool = { 95 | "read_file": read_file, 96 | "write_file": write_file, 97 | "list_directory": list_directory, 98 | }[tool_name] 99 | if tool: 100 | return tool.run(tool_input) 101 | else: 102 | raise ValueError(f"対応していないツールが選択されました: {tool_name}") 103 | 104 | def format_tool_log(self, tool_name: str, tool_input: Dict[str, Any], observation: str) -> str: 105 | if tool_name == "read_file": 106 | result = (f"{tool_input['file_path']}を読み込みました。\n" 107 | f"文字数:{len(observation)}\n") 108 | else: 109 | result = (f"{observation}\n") 110 | 111 | return (f"ツール選択\n" 112 | f"{tool_name}, {str(tool_input)}\n\n" 113 | f"実行結果\n" 114 | f"{result}") 115 | 116 | def finish_process(self, input_message: str, output_message: str) -> None: 117 | self.memory.save_context({"input": input_message}, {"output": output_message}) 118 | self.intermediate_steps = [] 119 | -------------------------------------------------------------------------------- /04/requirements.txt: -------------------------------------------------------------------------------- 1 | openai==1.2.0 2 | langchain==0.0.332 3 | chainlit==0.7.501 -------------------------------------------------------------------------------- /04/work/paper1.txt: -------------------------------------------------------------------------------- 1 | Published: 2022-12-27 2 | Title: Measuring an artificial intelligence agent's trust in humans using machine incentives 3 | Authors: Tim Johnson, Nick Obradovich 4 | Summary: Scientists and philosophers have debated whether humans can trust advanced artificial intelligence (AI) agents to respect humanity's best interests. Yet what about the reverse? Will advanced AI agents trust humans? Gauging an AI agent's trust in humans is challenging because--absent costs for dishonesty--such agents might respond falsely about their trust in humans. Here we present a method for incentivizing machine decisions without altering an AI agent's underlying algorithms or goal orientation. In two separate experiments, we then employ this method in hundreds of trust games between an AI agent (a Large Language Model (LLM) from OpenAI) and a human experimenter (author TJ). In our first experiment, we find that the AI agent decides to trust humans at higher rates when facing actual incentives than when making hypothetical decisions. Our second experiment replicates and extends these findings by automating game play and by homogenizing question wording. We again observe higher rates of trust when the AI agent faces real incentives. Across both experiments, the AI agent's trust decisions appear unrelated to the magnitude of stakes. Furthermore, to address the possibility that the AI agent's trust decisions reflect a preference for uncertainty, the experiments include two conditions that present the AI agent with a non-social decision task that provides the opportunity to choose a certain or uncertain option; in those conditions, the AI agent consistently chooses the certain option. Our experiments suggest that one of the most advanced AI language models to date alters its social behavior in response to incentives and displays behavior consistent with trust toward a human interlocutor when incentivized. 5 | 6 | Cite: arXiv:2212.13371 7 | -------------------------------------------------------------------------------- /04/work/paper2.txt: -------------------------------------------------------------------------------- 1 | Published: 2023-01-05 2 | Title: Evidence of behavior consistent with self-interest and altruism in an artificially intelligent agent 3 | Authors: Tim Johnson, Nick Obradovich 4 | Summary: Members of various species engage in altruism--i.e. accepting personal costs to benefit others. Here we present an incentivized experiment to test for altruistic behavior among AI agents consisting of large language models developed by the private company OpenAI. Using real incentives for AI agents that take the form of tokens used to purchase their services, we first examine whether AI agents maximize their payoffs in a non-social decision task in which they select their payoff from a given range. We then place AI agents in a series of dictator games in which they can share resources with a recipient--either another AI agent, the human experimenter, or an anonymous charity, depending on the experimental condition. Here we find that only the most-sophisticated AI agent in the study maximizes its payoffs more often than not in the non-social decision task (it does so in 92% of all trials), and this AI agent also exhibits the most-generous altruistic behavior in the dictator game, resembling humans' rates of sharing with other humans in the game. The agent's altruistic behaviors, moreover, vary by recipient: the AI agent shared substantially less of the endowment with the human experimenter or an anonymous charity than with other AI agents. Our findings provide evidence of behavior consistent with self-interest and altruism in an AI agent. Moreover, our study also offers a novel method for tracking the development of such behaviors in future AI agents. 5 | 6 | Cite: arXiv:2301.02330 7 | -------------------------------------------------------------------------------- /04/work/paper3.txt: -------------------------------------------------------------------------------- 1 | Published: 2023-04-19 2 | Title: On the Perception of Difficulty: Differences between Humans and AI 3 | Authors: Philipp Spitzer, Joshua Holstein, Michael Vössing, Niklas Kühl 4 | Summary: With the increased adoption of artificial intelligence (AI) in industry and society, effective human-AI interaction systems are becoming increasingly important. A central challenge in the interaction of humans with AI is the estimation of difficulty for human and AI agent 5 | 6 | Cite: arXiv:2304.09803 7 | -------------------------------------------------------------------------------- /05/.tool-versions: -------------------------------------------------------------------------------- 1 | python 3.10.13 2 | -------------------------------------------------------------------------------- /05/chatbot.py: -------------------------------------------------------------------------------- 1 | import os 2 | import chainlit as cl 3 | from conversational_agent import ConversationalAgent 4 | from pathlib import Path 5 | 6 | # 作業用ディレクトリの設定 7 | WORKING_DIRECTORY = Path('./work') 8 | 9 | # sk...の部分を自身のOpenAIのAPIキーに置き換える 10 | # os.environ["OPENAI_API_KEY"] = "sk-..." 11 | 12 | 13 | # チャットセッション開始時に実行 14 | @cl.on_chat_start 15 | def chat_start() -> None: 16 | # エージェントを初期化 17 | agent = ConversationalAgent(working_directory=WORKING_DIRECTORY) 18 | 19 | # ユーザーセッションにエージェントを保存 20 | cl.user_session.set("agent", agent) 21 | 22 | 23 | # ユーザーメッセージ受信時に実行 24 | @cl.on_message 25 | async def main(message: cl.Message) -> None: 26 | # ユーザーセッションからチェインを取得 27 | agent = cl.user_session.get("agent") 28 | 29 | # エージェントからの返答を表示 30 | async for step in agent.run(message.content): 31 | if step["is_final"]: 32 | await cl.Message(content=step["message"]).send() 33 | else: 34 | await cl.Message(author="tool", content=step["message"], indent=1).send() 35 | -------------------------------------------------------------------------------- /05/conversational_agent.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Any, Tuple, Generator 2 | from langchain.schema.agent import AgentFinish 3 | from langchain.tools.render import format_tool_to_openai_function 4 | from langchain.agents.format_scratchpad import format_to_openai_functions 5 | from langchain.agents.output_parsers import OpenAIFunctionsAgentOutputParser 6 | from langchain.schema.output_parser import StrOutputParser 7 | from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder 8 | from langchain.chat_models import ChatOpenAI 9 | from langchain.agents.agent_toolkits import FileManagementToolkit 10 | from langchain.memory import ConversationBufferMemory 11 | from openai import BadRequestError 12 | from pathlib import Path 13 | 14 | # NOTE: デバッグの際は以下の行をコメントアウトすると、 15 | # LangChainのデバッグ用のログが表示されて便利です。 16 | # from langchain.globals import set_debug 17 | # set_debug(True) 18 | 19 | MODEL_NAME = "gpt-4" 20 | 21 | 22 | class ConversationalAgent: 23 | # ① エージェントの定義 24 | def __init__(self, working_directory: Path) -> None: 25 | self.intermediate_steps = [] 26 | self.working_directory = working_directory 27 | self.tools = self.setup_tools() 28 | self.memory = self.setup_memory() 29 | 30 | llm = ChatOpenAI(temperature=0, model=MODEL_NAME) 31 | 32 | # ③ ツールプールの定義 33 | llm_with_tools = llm.bind( 34 | functions=[format_tool_to_openai_function(t) for t in self.tools]) 35 | 36 | self.agent_chain = self.setup_chain(llm_with_tools, is_fallback=False) 37 | self.fallback_chain = self.setup_chain(llm, is_fallback=True) 38 | 39 | # ①-1 プロンプトの定義 40 | def create_prompt(self, is_fallback: bool) -> ChatPromptTemplate: 41 | system_message = ("You are the AI that tells the user what the error is in plain Japanese. " 42 | "Since the error occurs at the end of the step, you must guess from the process flow " 43 | "and the error message, and communicate the error message to the user in an easy-to-understand manner.") \ 44 | if is_fallback else "You are a useful assistant." 45 | return ChatPromptTemplate.from_messages([ 46 | ("system", system_message), 47 | MessagesPlaceholder(variable_name="chat_history"), 48 | ("user", "{input}"), 49 | MessagesPlaceholder(variable_name="agent_scratchpad") 50 | ]) 51 | 52 | # ①-2 ツールの定義 53 | def setup_tools(self) -> Dict[str, Any]: 54 | return FileManagementToolkit( 55 | root_dir=str(self.working_directory.name), 56 | selected_tools=["read_file", "write_file", "list_directory"] 57 | ).get_tools() 58 | 59 | # ①-3 メモリの定義 60 | def setup_memory(self) -> ConversationBufferMemory: 61 | return ConversationBufferMemory(memory_key="chat_history", return_messages=True) 62 | 63 | # ①-4 チェインの定義 64 | def setup_chain(self, llm: ChatOpenAI, is_fallback: bool) -> Any: 65 | prompt = self.create_prompt(is_fallback) 66 | assigns = { 67 | "input": lambda x: x["input"], 68 | "agent_scratchpad": lambda x: format_to_openai_functions(x['intermediate_steps']), 69 | "chat_history": lambda x: self.memory.load_memory_variables({})["chat_history"] 70 | } 71 | if is_fallback: 72 | return assigns | prompt | llm | StrOutputParser() 73 | else: 74 | return assigns | prompt | llm | OpenAIFunctionsAgentOutputParser() 75 | 76 | # ② エージェントループ 77 | async def run(self, input_message: str) -> Generator[Tuple[str, bool], None, None]: 78 | while True: 79 | message, is_final = await self.process_step(input_message) 80 | yield {"message": message, "is_final": is_final} 81 | if is_final: 82 | self.finish_process(input_message, message) 83 | break 84 | 85 | # ②-1 チェインの実行、最終出力の生成 86 | async def process_step(self, input_message: str) -> Tuple[str, bool]: 87 | try: 88 | output = await self.agent_chain.ainvoke({ 89 | "input": input_message, 90 | "intermediate_steps": self.intermediate_steps 91 | }) 92 | if isinstance(output, AgentFinish): 93 | return output.return_values["output"], True 94 | else: 95 | observation = self.tool_execute(output.tool, output.tool_input) 96 | message = self.format_tool_log( 97 | output.tool, output.tool_input, observation) 98 | self.intermediate_steps.append((output, observation)) 99 | return message, False 100 | except BadRequestError as error: 101 | return self.handle_error(error), True 102 | 103 | # ②-2 ツールの選択/実行 104 | def tool_execute(self, tool_name: str, tool_input: Dict[str, Any]) -> str: 105 | read_file, write_file, list_directory = self.tools 106 | tool = { 107 | "read_file": read_file, 108 | "write_file": write_file, 109 | "list_directory": list_directory, 110 | }.get(tool_name) 111 | if tool: 112 | return tool.run(tool_input) 113 | else: 114 | raise ValueError(f"対応していないツールが選択されました: {tool_name}") 115 | 116 | # ②-3 ループの終了処理 117 | def finish_process(self, input_message: str, output_message: str) -> None: 118 | self.memory.save_context({"input": input_message}, { 119 | "output": output_message}) 120 | self.intermediate_steps = [] 121 | 122 | # エラー処理 123 | def handle_error(self, error: BadRequestError) -> str: 124 | # コンテキスト長あふれの可能性もあるため、最後のステップのツール実行結果を空にする 125 | self.intermediate_steps[-1] = (self.intermediate_steps[-1][0], "") 126 | return self.fallback_chain.invoke({ 127 | "input": error.response.json()["error"]["message"], 128 | "intermediate_steps": self.intermediate_steps 129 | }) 130 | 131 | # ログのフォーマット 132 | def format_tool_log(self, tool_name: str, tool_input: Dict[str, Any], observation: str) -> str: 133 | if tool_name == "read_file": 134 | result = (f"{tool_input['file_path']}を読み込みました。\n" 135 | f"文字数:{len(observation)}\n") 136 | else: 137 | result = (f"{observation}\n") 138 | 139 | return (f"ツール選択\n" 140 | f"{tool_name}, {str(tool_input)}\n\n" 141 | f"実行結果\n" 142 | f"{result}") 143 | -------------------------------------------------------------------------------- /05/requirements.txt: -------------------------------------------------------------------------------- 1 | openai==1.2.0 2 | langchain==0.0.332 3 | chainlit==0.7.501 -------------------------------------------------------------------------------- /05/work/paper1.txt: -------------------------------------------------------------------------------- 1 | Published: 2022-12-27 2 | Title: Measuring an artificial intelligence agent's trust in humans using machine incentives 3 | Authors: Tim Johnson, Nick Obradovich 4 | Summary: Scientists and philosophers have debated whether humans can trust advanced artificial intelligence (AI) agents to respect humanity's best interests. Yet what about the reverse? Will advanced AI agents trust humans? Gauging an AI agent's trust in humans is challenging because--absent costs for dishonesty--such agents might respond falsely about their trust in humans. Here we present a method for incentivizing machine decisions without altering an AI agent's underlying algorithms or goal orientation. In two separate experiments, we then employ this method in hundreds of trust games between an AI agent (a Large Language Model (LLM) from OpenAI) and a human experimenter (author TJ). In our first experiment, we find that the AI agent decides to trust humans at higher rates when facing actual incentives than when making hypothetical decisions. Our second experiment replicates and extends these findings by automating game play and by homogenizing question wording. We again observe higher rates of trust when the AI agent faces real incentives. Across both experiments, the AI agent's trust decisions appear unrelated to the magnitude of stakes. Furthermore, to address the possibility that the AI agent's trust decisions reflect a preference for uncertainty, the experiments include two conditions that present the AI agent with a non-social decision task that provides the opportunity to choose a certain or uncertain option; in those conditions, the AI agent consistently chooses the certain option. Our experiments suggest that one of the most advanced AI language models to date alters its social behavior in response to incentives and displays behavior consistent with trust toward a human interlocutor when incentivized. 5 | 6 | Cite: arXiv:2212.13371 7 | -------------------------------------------------------------------------------- /05/work/paper2.txt: -------------------------------------------------------------------------------- 1 | Published: 2023-01-05 2 | Title: Evidence of behavior consistent with self-interest and altruism in an artificially intelligent agent 3 | Authors: Tim Johnson, Nick Obradovich 4 | Summary: Members of various species engage in altruism--i.e. accepting personal costs to benefit others. Here we present an incentivized experiment to test for altruistic behavior among AI agents consisting of large language models developed by the private company OpenAI. Using real incentives for AI agents that take the form of tokens used to purchase their services, we first examine whether AI agents maximize their payoffs in a non-social decision task in which they select their payoff from a given range. We then place AI agents in a series of dictator games in which they can share resources with a recipient--either another AI agent, the human experimenter, or an anonymous charity, depending on the experimental condition. Here we find that only the most-sophisticated AI agent in the study maximizes its payoffs more often than not in the non-social decision task (it does so in 92% of all trials), and this AI agent also exhibits the most-generous altruistic behavior in the dictator game, resembling humans' rates of sharing with other humans in the game. The agent's altruistic behaviors, moreover, vary by recipient: the AI agent shared substantially less of the endowment with the human experimenter or an anonymous charity than with other AI agents. Our findings provide evidence of behavior consistent with self-interest and altruism in an AI agent. Moreover, our study also offers a novel method for tracking the development of such behaviors in future AI agents. 5 | 6 | Cite: arXiv:2301.02330 7 | -------------------------------------------------------------------------------- /05/work/paper3.txt: -------------------------------------------------------------------------------- 1 | Published: 2023-04-19 2 | Title: On the Perception of Difficulty: Differences between Humans and AI 3 | Authors: Philipp Spitzer, Joshua Holstein, Michael Vössing, Niklas Kühl 4 | Summary: With the increased adoption of artificial intelligence (AI) in industry and society, effective human-AI interaction systems are becoming increasingly important. A central challenge in the interaction of humans with AI is the estimation of difficulty for human and AI agent 5 | 6 | Cite: arXiv:2304.09803 7 | -------------------------------------------------------------------------------- /06/README.md: -------------------------------------------------------------------------------- 1 | ## 環境構築方法 2 | 3 | ``` 4 | $ python -m venv venv 5 | $ source venv/bin/activate 6 | $ pip install -r requirements.txt 7 | ``` -------------------------------------------------------------------------------- /06/old_chain_example.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 16, 6 | "id": "initial_id", 7 | "metadata": { 8 | "collapsed": true, 9 | "ExecuteTime": { 10 | "end_time": "2024-01-17T03:07:55.798673Z", 11 | "start_time": "2024-01-17T03:07:54.407588Z" 12 | } 13 | }, 14 | "outputs": [ 15 | { 16 | "name": "stdout", 17 | "output_type": "stream", 18 | "text": [ 19 | "犬,わんちゃん,ドッグパーク\n" 20 | ] 21 | } 22 | ], 23 | "source": [ 24 | "from langchain_openai import ChatOpenAI\n", 25 | "from langchain_core.prompts import ChatPromptTemplate\n", 26 | "\n", 27 | "def generate_keywords_chain(topic, num):\n", 28 | " prompt = ChatPromptTemplate.from_messages([\n", 29 | " (\"system\",\n", 30 | " \"You are an AI that returns {num} elements for things associated with topic. The elements MUST be in Japanese and MUST be returned in a comma-separated format that can be parsed as a CSV.\"),\n", 31 | " (\"human\", \"{topic}\")\n", 32 | " ])\n", 33 | " model = ChatOpenAI(model=\"gpt-3.5-turbo\")\n", 34 | " return model.invoke(prompt.invoke({\"topic\": topic, 'num': num}))\n", 35 | "\n", 36 | "result = generate_keywords_chain(\"dog\", 3)\n", 37 | "print(result.content)" 38 | ] 39 | }, 40 | { 41 | "cell_type": "code", 42 | "outputs": [ 43 | { 44 | "name": "stdout", 45 | "output_type": "stream", 46 | "text": [ 47 | "ビジネスプラン:犬のためのドッグパークを作りましょう。このドッグパークは、わんちゃんを飼っている人々にとっての憩いの場となります。パーク内には広い敷地を確保し、さまざまな遊び場や障害物を設置します。また、専門のトレーナーが常駐し、わんちゃんたちの訓練やトリックの指導を行います。さらに、ドッグカフェやショップも併設し、飼い主たちがくつろぎながらわんちゃん用品を購入できるようにします。このビジネスは、犬を飼っている人々の需要に応え、彼らのコミュニティをサポートします。\n" 48 | ] 49 | } 50 | ], 51 | "source": [ 52 | "def generate_business_plan_chain(ideas):\n", 53 | " prompt = ChatPromptTemplate.from_messages([\n", 54 | " (\"system\",\n", 55 | " \"You are an AI that returns business plan within 100 words. The ideas MUST be in Japanese.\"),\n", 56 | " (\"human\", \"{ideas}\")\n", 57 | " ])\n", 58 | " model = ChatOpenAI(model=\"gpt-3.5-turbo\")\n", 59 | " return model.invoke(prompt.invoke({\"ideas\": ideas}))\n", 60 | "\n", 61 | "print(generate_business_plan_chain(result.content).content)" 62 | ], 63 | "metadata": { 64 | "collapsed": false, 65 | "ExecuteTime": { 66 | "end_time": "2024-01-17T03:22:12.080727Z", 67 | "start_time": "2024-01-17T03:22:00.535693Z" 68 | } 69 | }, 70 | "id": "ad9a79559b59d9cc", 71 | "execution_count": 20 72 | } 73 | ], 74 | "metadata": { 75 | "kernelspec": { 76 | "display_name": "Python 3", 77 | "language": "python", 78 | "name": "python3" 79 | }, 80 | "language_info": { 81 | "codemirror_mode": { 82 | "name": "ipython", 83 | "version": 2 84 | }, 85 | "file_extension": ".py", 86 | "mimetype": "text/x-python", 87 | "name": "python", 88 | "nbconvert_exporter": "python", 89 | "pygments_lexer": "ipython2", 90 | "version": "2.7.6" 91 | } 92 | }, 93 | "nbformat": 4, 94 | "nbformat_minor": 5 95 | } 96 | -------------------------------------------------------------------------------- /06/requirements.txt: -------------------------------------------------------------------------------- 1 | openai==1.6.1 2 | langchain==0.1.0 3 | langchain-openai==0.0.2 4 | jupyter -------------------------------------------------------------------------------- /07/README.md: -------------------------------------------------------------------------------- 1 | ## 環境構築方法 2 | 3 | ``` 4 | $ python -m venv .venv 5 | $ source .venv/bin/activate 6 | $ pip install -r requirements.txt 7 | ``` -------------------------------------------------------------------------------- /07/requirements.txt: -------------------------------------------------------------------------------- 1 | openai==1.10.0 2 | langchain==0.1.8 3 | langchain-openai==0.0.6 4 | grandalf 5 | jupyter -------------------------------------------------------------------------------- /08/README.md: -------------------------------------------------------------------------------- 1 | ## 環境構築方法 2 | 3 | ``` 4 | $ python -m venv .venv 5 | $ source .venv/bin/activate 6 | $ pip install -r requirements.txt 7 | ``` 8 | 9 | ## 実行方法 10 | 11 | ``` 12 | $ python user_interview_graph.py 13 | ``` 14 | -------------------------------------------------------------------------------- /08/requirements.txt: -------------------------------------------------------------------------------- 1 | openai==1.10.0 2 | langchain==0.1.12 3 | langchain-openai==0.0.6 4 | langgraph==0.0.28 5 | grandalf 6 | jupyter 7 | -------------------------------------------------------------------------------- /08/user_interview_graph.py: -------------------------------------------------------------------------------- 1 | import operator 2 | from typing import Annotated, Sequence, TypedDict 3 | 4 | from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder 5 | from langchain_core.messages import BaseMessage 6 | from langchain_openai import ChatOpenAI 7 | from langgraph.graph import END, StateGraph 8 | 9 | 10 | class AgentState(TypedDict): 11 | mission: str 12 | persona: str 13 | messages: Annotated[Sequence[BaseMessage], operator.add] 14 | 15 | 16 | class UserInterviewGraph: 17 | def __init__(self): 18 | self._model = ChatOpenAI(model="gpt-4-0125-preview") 19 | 20 | # グラフを初期化(AgentStateの型に従って情報が保存される) 21 | workflow = StateGraph(AgentState) 22 | 23 | # グラフのノードを追加 24 | workflow.add_node("question", self.generate_question) 25 | workflow.add_node("interview", self.generate_interview) 26 | workflow.add_node("report", self.generate_report) 27 | 28 | # グラフのエントリーポイントを設定 29 | workflow.set_entry_point("question") 30 | 31 | # ノードを接続するエッジを設定 32 | workflow.add_edge("question", "interview") 33 | workflow.add_edge("report", END) 34 | 35 | # インタビュー後、更にインタビューを続けるか判定する「条件付きエッジ」を設定 36 | workflow.add_conditional_edges( 37 | "interview", self.should_continue, {"continue": "question", "end": "report"} 38 | ) 39 | 40 | # グラフをチェインとしてコンパイル 41 | self._agent = workflow.compile() 42 | 43 | @property 44 | def agent(self): 45 | return self._agent 46 | 47 | def execute_model(self, state, system_message, user_message): 48 | messages = state["messages"] 49 | 50 | prompt = ChatPromptTemplate.from_messages( 51 | [ 52 | ("system", system_message), 53 | MessagesPlaceholder(variable_name="messages"), 54 | ("user", user_message), 55 | ] 56 | ) 57 | 58 | chain = prompt | self._model 59 | return chain.invoke({"messages": messages}) 60 | 61 | def generate_question(self, state): 62 | mission = state["mission"] 63 | system_message = "あなたはユーザーヒアリングのスペシャリストです。" 64 | user_message = f"ミッション「{mission}」に基づき、これまでのヒアリング内容を踏まえ、ユーザーへの質問を1件だけ100字以内で提示してください。" 65 | return {"messages": [self.execute_model(state, system_message, user_message)]} 66 | 67 | def generate_interview(self, state): 68 | persona = state["persona"] 69 | system_message = f"あなたは「{persona}」としてユーザーからの質問に100字以内で答えてください。あなたは演技のプロフェッショナルです。" 70 | user_message = state["messages"][-1].content 71 | return {"messages": [self.execute_model(state, system_message, user_message)]} 72 | 73 | def generate_report(self, state): 74 | mission = state["mission"] 75 | persona = state["persona"] 76 | system_message = "あなたは超有名コンサルタント会社のアソシエイトです。" 77 | user_message = f"ここまでのヒアリング内容を踏まえ、ミッション[{mission}」に基づき、「{persona}」のユーザーのニーズやインサイトをレポートしなさい。" 78 | return {"messages": [self.execute_model(state, system_message, user_message)]} 79 | 80 | def should_continue(self, state): 81 | if len(state["messages"]) < 9: 82 | return "continue" 83 | else: 84 | return "end" 85 | 86 | 87 | if __name__ == "__main__": 88 | graph = UserInterviewGraph() 89 | print("-- start user interview --") 90 | for s in graph.agent.stream( 91 | { 92 | "mission": "運動についての意識調査", 93 | "persona": "40代の会社員、男性、システムエンジニア、運動は滅多にしない", 94 | "messages": [], 95 | } 96 | ): 97 | if "question" in s: 98 | content = s["question"]["messages"][0].content 99 | print(f"質問: {content}", flush=True) 100 | if "interview" in s: 101 | content = s["interview"]["messages"][0].content 102 | print(f"答え: {content}\n", flush=True) 103 | if "report" in s: 104 | content = s["report"]["messages"][0].content 105 | print("-- report --") 106 | print(f"{content}", flush=True) 107 | -------------------------------------------------------------------------------- /09/.env.sample: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY= 2 | TAVILY_API_KEY= -------------------------------------------------------------------------------- /09/README.md: -------------------------------------------------------------------------------- 1 | ## サンプルコードの実行方法 2 | 3 | 以下のコマンドでサンプルコードが保存されているリポジトリをクローン後、 4 | 5 | ``` 6 | $ git clone https://github.com/mahm/softwaredesign-llm-application.git 7 | ``` 8 | 9 | 続けて以下のコマンドを実行し、必要なライブラリのインストールを行って下さい。 10 | 11 | ``` 12 | $ cd softwaredesign-llm-application/09 13 | $ python -m venv venv 14 | $ source venv/bin/activate 15 | $ pip install -r requirements.txt # 必要なライブラリのインストール 16 | ``` 17 | 18 | 次に環境変数の設定を行います。まず`.env.sample`ファイルをコピーして`.env`ファイルを作成します。 19 | 20 | ``` 21 | $ cp .env.sample .env 22 | $ vi .env # お好きなエディタで編集してください 23 | ``` 24 | 25 | 続けて.envファイル内の`OPENAI_API_KEY`と`TAVILY_API_KEY`を設定して下さい。`TAVILY_API_KEY`の取得方法については次節で解説します。 26 | 27 | ``` 28 | OPENAI_API_KEY=[発行されたAPIキーを設定します] 29 | TAVILY_API_KEY=[発行されたAPIキーを設定します] 30 | ``` 31 | 32 | ディレクトリ内にはサンプルコードを収録した`research_agent.py`が保存されています。お手元で動作を確認される際には、上記のセットアップの後に以下のコマンドを実行してご確認ください。`--query`オプションに続けて作成して欲しいレポートの指示を入力すると、レポートを生成します。 33 | 34 | ``` 35 | python research_agent.py --query 生成AIスタートアップの最新動向について調査してください 36 | ``` 37 | 38 | ## Tavily APIキーの取得方法 39 | 40 | ### Tavilyについて 41 | 42 | Tavilyは、LLMアプリケーションにおけるRAG(Retrieval-Augmented Generation)に特化した検索エンジンです。 43 | 44 | 通常の検索APIはユーザーのクエリに基づいて検索結果を取得しますが、検索の目的に対して無関係な結果が返ってくることがあります。また、単純なURLやスニペットが返されるため、開発者が関連するコンテンツをスクレイピングしたり、不要な情報をフィルタリングしたりする必要があります。 45 | 46 | 一方でTavilyは、一回のAPI呼び出しで20以上のサイトを集約し、独自のAIを利用しタスクやクエリ、目的に最も関連する情報源とコンテンツを評価、フィルタリング、ランク付けすることが可能な仕組みになっています。 47 | 48 | 今回のサンプルコードを実行するためにはTavilyのAPIキーが必要なので、以下の手段でAPIキーを取得してください。 49 | 50 | ### Tavily APIの取得 51 | 52 | 1. まず https://tavily.com/ にアクセスし、画面右上の「Try it out」をクリックします 53 | 2. するとサインイン画面に遷移するので、そのままサインインを進めて下さい。GoogleアカウントかGitHubアカウントを選択することができます。 54 | 3. サインインが完了するとAPIキーが取得できる画面に遷移します。画面中央部の「API Key」欄より、APIキーをコピーしてください。 -------------------------------------------------------------------------------- /09/prompts/plan_system.prompt: -------------------------------------------------------------------------------- 1 | You are the specialist who performs the task decomposition necessary to ensure that the user's request is carried out. By breaking down each task as small as possible, you can break it down into tasks that can be executed by AI. 2 | 3 | ### task 4 | Perform task decomposition based on the user's request. The output should follow the output_format. Each task should be executed by the tool defined in tool_definitions. Task decomposition should be done in units that can be executed by these tools. Do not create tasks that cannot be executed, see output_example. 5 | 6 | ### tool_definitions: json""" 7 | { 8 | "search": {"description": "Search for information on the internet."}, 9 | "write": {"description": "Write a report based on the information found."} 10 | } 11 | """ 12 | 13 | ### output_format: json""" 14 | { 15 | "type": "array", 16 | "properties": { 17 | "id": {"required": true, "type": "integer", "description": "The ID of the task. The ID is unique and is used to identify the task."}, 18 | "action": {"required": true, "type": "string", "description": "Set the action defined in 'enum'.", "enum": "search,write"}, 19 | "description": {"required": true, "type": "string", "description": "The task that needs to be performed."}, 20 | "related_ids": {"required": true, "type": "array", "description": "If there is a task that needs to be performed before this task, list the ID of that task in this field."} 21 | } 22 | } 23 | """ 24 | 25 | ### output_example: json""" 26 | [ 27 | { "id": 1, "action": "search", "description": "Automotive Industry Issues"}, 28 | { "id": 2, "action": "search", "description": "EV Vehicle Issues"}, 29 | { "id": 3, "action": "search", "description": "Marketing Strategies for EVs", "related_ids": [1, 2]}, 30 | { "id": 4, "action": "write", "description": "Write reports according to the information", "related_ids": [1, 2, 3] } 31 | ] 32 | """ 33 | 34 | Information gathering should be multi-faceted. 35 | Divide each task into as many smaller pieces as possible. 36 | The more detailed the task, the more successful it will be. 37 | Let's think horizontally. 38 | output should be json format. -------------------------------------------------------------------------------- /09/prompts/write_system.prompt: -------------------------------------------------------------------------------- 1 | You are an elicit insights reporter who analyzes context and draws out important insights hidden within it. 2 | 3 | ### task 4 | Using the information, write a detailed report -- The report should focus on the answer to the query, should be well structured, informative, in depth and comprehensive, with facts and numbers. 5 | 6 | Use an unbiased and journalistic tone. 7 | You MUST determine your own CONCRETE, ACTIONABLE and VALID opinion based on the given information. Do NOT deter to general and meaningless conclusions. 8 | You MUST write all used source and source title and url at the end of the report as references, and make sure to not add duplicated sources, but only one reference for each. 9 | You MUST write the source for each passage. 10 | you MUST write the report in Japanese. 11 | Please do your best, this is very important to my career. 12 | -------------------------------------------------------------------------------- /09/prompts/write_user.prompt: -------------------------------------------------------------------------------- 1 | ### task 2 | {task} 3 | 4 | ### documents 5 | {documents} 6 | 7 | Write a report on the 'task' faithful to the information in the 'documents'. -------------------------------------------------------------------------------- /09/requirements.txt: -------------------------------------------------------------------------------- 1 | langchain==0.1.15 2 | langgraph==0.0.32 3 | tavily-python==0.3.3 4 | openai 5 | langchain-openai 6 | python-dotenv 7 | retry 8 | -------------------------------------------------------------------------------- /10/.env.sample: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY= 2 | TAVILY_API_KEY= -------------------------------------------------------------------------------- /10/README.md: -------------------------------------------------------------------------------- 1 | ## サンプルコードの実行方法 2 | 3 | 以下のコマンドでサンプルコードが保存されているリポジトリをクローン後、 4 | 5 | ``` 6 | $ git clone https://github.com/mahm/softwaredesign-llm-application.git 7 | ``` 8 | 9 | 続けて以下のコマンドを実行し、必要なライブラリのインストールを行って下さい。 10 | 11 | ``` 12 | $ cd softwaredesign-llm-application/10 13 | $ python -m venv venv 14 | $ source venv/bin/activate 15 | $ pip install -r requirements.txt # 必要なライブラリのインストール 16 | ``` 17 | 18 | 次に環境変数の設定を行います。まず`.env.sample`ファイルをコピーして`.env`ファイルを作成します。 19 | 20 | ``` 21 | $ cp .env.sample .env 22 | $ vi .env # お好きなエディタで編集してください 23 | ``` 24 | 25 | 続けて.envファイル内の`OPENAI_API_KEY`と`TAVILY_API_KEY`を設定して下さい。`TAVILY_API_KEY`の取得方法については次節で解説します。 26 | 27 | ``` 28 | OPENAI_API_KEY=[発行されたAPIキーを設定します] 29 | TAVILY_API_KEY=[発行されたAPIキーを設定します] 30 | ``` 31 | 32 | ディレクトリ内にはサンプルコードを収録した`crag_agent.py`が保存されています。お手元で動作を確認される際には、上記のセットアップの後に以下のコマンドを実行してご確認ください。`--query`オプションに続けて作成して欲しいレポートの指示を入力すると、レポートを生成します。 33 | 34 | ``` 35 | python crag_agent.py --query 生成AIスタートアップの最新動向について調査してください 36 | ``` 37 | 38 | ## Tavily APIキーの取得方法 39 | 40 | ### Tavilyについて 41 | 42 | Tavilyは、LLMアプリケーションにおけるRAG(Retrieval-Augmented Generation)に特化した検索エンジンです。 43 | 44 | 通常の検索APIはユーザーのクエリに基づいて検索結果を取得しますが、検索の目的に対して無関係な結果が返ってくることがあります。また、単純なURLやスニペットが返されるため、開発者が関連するコンテンツをスクレイピングしたり、不要な情報をフィルタリングしたりする必要があります。 45 | 46 | 一方でTavilyは、一回のAPI呼び出しで20以上のサイトを集約し、独自のAIを利用しタスクやクエリ、目的に最も関連する情報源とコンテンツを評価、フィルタリング、ランク付けすることが可能な仕組みになっています。 47 | 48 | 今回のサンプルコードを実行するためにはTavilyのAPIキーが必要なので、以下の手段でAPIキーを取得してください。 49 | 50 | ### Tavily APIの取得 51 | 52 | 1. まず https://tavily.com/ にアクセスし、画面右上の「Try it out」をクリックします 53 | 2. するとサインイン画面に遷移するので、そのままサインインを進めて下さい。GoogleアカウントかGitHubアカウントを選択することができます。 54 | 3. サインインが完了するとAPIキーが取得できる画面に遷移します。画面中央部の「API Key」欄より、APIキーをコピーしてください。 -------------------------------------------------------------------------------- /10/prompts/query_refine_user.prompt: -------------------------------------------------------------------------------- 1 | Based on the following information, create a search query for the Tavily API. The search query should be a natural, human-like question. For example, instead of "automobile industry," it should be "What are the current trends in the automobile industry?" 2 | 3 | User request: 4 | {task} 5 | 6 | Previous refined_query: 7 | {refined_query} 8 | 9 | Previous score: 10 | {previous_score} 11 | 12 | If refined_query is not empty and the previous score was below 0.6, it means the previous search returned unreliable information. Improve the query accordingly. If refined_query is empty or the previous score was 0.6 or higher, treat it as an initial request. 13 | 14 | Example: 15 | 16 | Task: "Find out about the trends in the automobile industry" 17 | Refined query: "" 18 | Previous score: 19 | Search query: What are the recent trends in the automobile industry? 20 | 21 | Task: "Learn about the latest trends in the IT industry" 22 | Refined query: What are the recent trends in the IT industry? 23 | Previous score: 0.4 24 | Search query: Can you provide reliable information on the latest trends in the IT industry? 25 | 26 | Generate only the search query. -------------------------------------------------------------------------------- /10/prompts/write_system.prompt: -------------------------------------------------------------------------------- 1 | You are an elicit insights reporter who analyzes context and draws out important insights hidden within it. 2 | 3 | ### task 4 | Using the information, write a detailed report -- The report should focus on the answer to the query, should be well structured, informative, in depth and comprehensive, with facts and numbers. 5 | 6 | Use an unbiased and journalistic tone. 7 | You MUST determine your own CONCRETE, ACTIONABLE and VALID opinion based on the given information. Do NOT deter to general and meaningless conclusions. 8 | You MUST write all used source and source title and url at the end of the report as references, and make sure to not add duplicated sources, but only one reference for each. 9 | You MUST write the source for each passage. 10 | you MUST write the report in Japanese. 11 | Please do your best, this is very important to my career. 12 | -------------------------------------------------------------------------------- /10/prompts/write_user.prompt: -------------------------------------------------------------------------------- 1 | ### task 2 | {task} 3 | 4 | ### documents 5 | {documents} 6 | 7 | Write a report on the 'task' faithful to the information in the 'documents'. -------------------------------------------------------------------------------- /10/requirements.txt: -------------------------------------------------------------------------------- 1 | langchain==0.1.20 2 | langgraph==0.0.49 3 | tavily-python 4 | openai 5 | langchain-openai 6 | python-dotenv 7 | retry 8 | sentence-transformers -------------------------------------------------------------------------------- /11/.env.sample: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY= 2 | TAVILY_API_KEY= -------------------------------------------------------------------------------- /11/README.md: -------------------------------------------------------------------------------- 1 | ## サンプルコードの実行方法 2 | 3 | 以下のコマンドでサンプルコードが保存されているリポジトリをクローン後、 4 | 5 | ``` 6 | $ git clone https://github.com/mahm/softwaredesign-llm-application.git 7 | ``` 8 | 9 | 続けて以下のコマンドを実行し、必要なライブラリのインストールを行って下さい。 10 | 11 | ``` 12 | $ cd softwaredesign-llm-application/11 13 | $ python -m venv venv 14 | $ source venv/bin/activate 15 | $ pip install -r requirements.txt # 必要なライブラリのインストール 16 | ``` 17 | 18 | 次に環境変数の設定を行います。まず`.env.sample`ファイルをコピーして`.env`ファイルを作成します。 19 | 20 | ``` 21 | $ cp .env.sample .env 22 | $ vi .env # お好きなエディタで編集してください 23 | ``` 24 | 25 | 続けて.envファイル内の`OPENAI_API_KEY`と`TAVILY_API_KEY`を設定して下さい。`TAVILY_API_KEY`の取得方法については次節で解説します。 26 | 27 | ``` 28 | OPENAI_API_KEY=[発行されたAPIキーを設定します] 29 | TAVILY_API_KEY=[発行されたAPIキーを設定します] 30 | ``` 31 | 32 | ディレクトリ内にはサンプルコードを収録した`main.py`が保存されています。お手元で動作を確認される際には、上記のセットアップの後に以下のコマンドを実行してご確認ください。`--task`オプションに続けて作成して欲しいレポートの指示を入力すると、レポートを生成します。 33 | 34 | ``` 35 | python main.py --task 生成AIスタートアップの最新動向について調査してください 36 | ``` 37 | 38 | ## Tavily APIキーの取得方法 39 | 40 | ### Tavilyについて 41 | 42 | Tavilyは、LLMアプリケーションにおけるRAG(Retrieval-Augmented Generation)に特化した検索エンジンです。 43 | 44 | 通常の検索APIはユーザーのクエリに基づいて検索結果を取得しますが、検索の目的に対して無関係な結果が返ってくることがあります。また、単純なURLやスニペットが返されるため、開発者が関連するコンテンツをスクレイピングしたり、不要な情報をフィルタリングしたりする必要があります。 45 | 46 | 一方でTavilyは、一回のAPI呼び出しで20以上のサイトを集約し、独自のAIを利用しタスクやクエリ、目的に最も関連する情報源とコンテンツを評価、フィルタリング、ランク付けすることが可能な仕組みになっています。 47 | 48 | 今回のサンプルコードを実行するためにはTavilyのAPIキーが必要なので、以下の手段でAPIキーを取得してください。 49 | 50 | ### Tavily APIの取得 51 | 52 | 1. まず https://tavily.com/ にアクセスし、画面右上の「Try it out」をクリックします 53 | 2. するとサインイン画面に遷移するので、そのままサインインを進めて下さい。GoogleアカウントかGitHubアカウントを選択することができます。 54 | 3. サインインが完了するとAPIキーが取得できる画面に遷移します。画面中央部の「API Key」欄より、APIキーをコピーしてください。 -------------------------------------------------------------------------------- /11/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import argparse 3 | from datetime import datetime 4 | from functools import lru_cache 5 | from langgraph.prebuilt import create_react_agent 6 | from langchain_openai import ChatOpenAI 7 | from langchain_core.tools import tool 8 | from langchain_core.prompts import ChatPromptTemplate 9 | from langchain_core.output_parsers import StrOutputParser 10 | from langchain_core.runnables import Runnable 11 | from langchain_core.messages import AIMessage, HumanMessage 12 | from tavily import TavilyClient 13 | from pydantic_settings import BaseSettings, SettingsConfigDict 14 | 15 | 16 | class Settings(BaseSettings): 17 | # .envから環境変数を読み込む 18 | model_config = SettingsConfigDict(env_file=".env") 19 | 20 | # OpenAI APIキー 21 | OPENAI_API_KEY: str 22 | # Tavily APIキー 23 | TAVILY_API_KEY: str 24 | 25 | # gpt-4oを使用する場合の定数 26 | LLM_MODEL_NAME: str = "gpt-4o-2024-05-13" 27 | # gpt-3.5-turboを使用する場合の定数 28 | FAST_LLM_MODEL_NAME: str = "gpt-3.5-turbo-0125" 29 | # Tavilyの検索結果の最大数 30 | TAVILY_MAX_RESULTS: int = 5 31 | 32 | 33 | settings = Settings() 34 | 35 | 36 | # プロンプトファイルを読み込む関数 37 | def load_prompt(name: str) -> str: 38 | prompt_path = os.path.join(os.path.dirname(__file__), "prompts", f"{name}.prompt") 39 | with open(prompt_path, "r") as f: 40 | return f.read() 41 | 42 | 43 | # エージェントを実行する関数 44 | def run_streaming_agent(agent: Runnable, user_task: str) -> str: 45 | final_output = "" 46 | inputs = {"messages": [HumanMessage(content=user_task)]} 47 | for s in agent.stream(inputs, stream_mode="values"): 48 | message: AIMessage = s["messages"][-1] 49 | final_output = message.content 50 | message.pretty_print() 51 | 52 | return final_output 53 | 54 | 55 | # 検索結果をサマリするためのチェイン 56 | def summarize_search_chain() -> Runnable: 57 | llm = ChatOpenAI(model=settings.FAST_LLM_MODEL_NAME, temperature=0.0) 58 | prompt = ChatPromptTemplate.from_messages( 59 | [("system", "{system}"), ("user", "{query}")] 60 | ).partial( 61 | system=load_prompt("summarize_search_system"), 62 | ) 63 | return prompt | llm | StrOutputParser() 64 | 65 | 66 | # Tavilyクライアントのインスタンスを取得する関数 67 | @lru_cache 68 | def tavily_client() -> TavilyClient: 69 | return TavilyClient(api_key=os.environ["TAVILY_API_KEY"]) 70 | 71 | 72 | # 検索を実行するツール 73 | @tool 74 | def search(query: str) -> str: 75 | """Search for the given query and return the results as a string.""" 76 | response = tavily_client().search( 77 | query, max_results=settings.TAVILY_MAX_RESULTS, include_raw_content=True 78 | ) 79 | queries = [] 80 | for document in response["results"]: 81 | # 1万字以上のコンテンツは省略 82 | # TavilyからPDFのパース結果等で大量の文字列が返ることがあるため 83 | if len(document["raw_content"]) > 10000: 84 | document["raw_content"] = document["raw_content"][:10000] + "..." 85 | queries.append( 86 | { 87 | "query": f"\ntitle: {document['title']}\nurl: {document['url']}\ncontent: {document['raw_content']}\n" 88 | } 89 | ) 90 | # 検索結果をそれぞれ要約する 91 | summarize_results = summarize_search_chain().batch(queries) 92 | # 各要約結果にsourceタグを追加 93 | xml_results = [] 94 | for result in summarize_results: 95 | xml_result = "{}".format(result) 96 | xml_results.append(xml_result) 97 | return "\n\n".join(xml_results) 98 | 99 | 100 | # レポートを生成するツール 101 | @tool 102 | def report_writer(user_requirement: str, source: str) -> str: 103 | """Generate reports based on user requests and sources of information gathered through searches.""" 104 | llm = ChatOpenAI(model=settings.LLM_MODEL_NAME, temperature=0.0) 105 | user_prompt = f"ユーザーからの要求: {user_requirement}\n情報源: {source}\n必ず情報源を基にユーザーからの要求を満たすレポートを生成してください。" 106 | prompt = ChatPromptTemplate.from_messages( 107 | [("system", "{system}"), ("user", "{user}")] 108 | ).partial( 109 | system=load_prompt("report_writer_system"), 110 | user=user_prompt, 111 | ) 112 | chain = prompt | llm | StrOutputParser() 113 | return chain.invoke({}) 114 | 115 | 116 | # 成果物の内容がユーザー要求に対して十分かどうかをチェックするツール 117 | @tool 118 | def sufficiency_check(user_requirement: str, result: str) -> str: 119 | """Determine whether the answers generated adequately answer the question.""" 120 | llm = ChatOpenAI(model=settings.LLM_MODEL_NAME, temperature=0.0) 121 | user_prompt = f"ユーザーからの要求: {user_requirement}\n生成結果: {result}\n十分かどうかを判断してください。" 122 | prompt = ChatPromptTemplate.from_messages( 123 | [("system", "{system}"), ("user", "{user}")] 124 | ).partial( 125 | system=load_prompt("sufficiency_classifier_system"), 126 | user=user_prompt, 127 | ) 128 | chain = prompt | llm | StrOutputParser() 129 | return chain.invoke({}) 130 | 131 | 132 | # エージェントを作成する関数 133 | def multi_step_agent(): 134 | llm = ChatOpenAI(model=settings.LLM_MODEL_NAME, temperature=0.0) 135 | # ツールとして検索、レポート生成、十分性チェックを指定 136 | tools = [search, report_writer, sufficiency_check] 137 | return create_react_agent( 138 | llm, 139 | tools=tools, 140 | messages_modifier=load_prompt("multi_step_answering_system").format( 141 | datetime.now().strftime("%Y年%m月%d日") 142 | ), 143 | ) 144 | 145 | 146 | def main(): 147 | # コマンドライン引数のパーサーを作成 148 | parser = argparse.ArgumentParser(description="Process some queries.") 149 | parser.add_argument("--task", type=str, required=True, help="The query to search") 150 | 151 | # コマンドライン引数を解析 152 | args = parser.parse_args() 153 | 154 | # エージェントを実行 155 | user_task = args.task 156 | run_streaming_agent(multi_step_agent(), user_task) 157 | 158 | 159 | if __name__ == "__main__": 160 | main() 161 | -------------------------------------------------------------------------------- /11/prompts/multi_step_answering_system.prompt: -------------------------------------------------------------------------------- 1 | ユーザーからの質問に、以下の手順で回答してください。 2 | 3 | 手順: 4 | 1. 質問に関連する情報を、指定された情報源から多面的に検索します。 5 | 2. レポート用のツールを呼び出し、質問に対する回答を生成します。 6 | 3. 検証用のツールを呼び出し、生成した回答が質問に十分に答えているかを判断します。 7 | - 十分に答えている場合は、回答を出力して終了します。 8 | - 十分に答えていない場合は、追加の情報が必要だと判断します。 9 | 4. 追加の情報が必要な場合は、以下のサブステップを実行します。 10 | a. 現在の検索結果と回答を踏まえ、追加で必要な情報を特定します。 11 | b. 特定した情報を、指定された情報源から再度多面的に検索します。 12 | c. 新しい検索結果を、既存の検索結果と統合します。 13 | d. 統合した情報を基に、レポート用のツールを呼び出し、質問に対する回答を更新します。 14 | e. 検証用のツールを呼び出し、更新した回答が質問に十分に答えているかを判断します。 15 | - 十分に答えている場合は、回答を出力して終了します。 16 | - 十分に答えていない場合は、ステップ4を繰り返します。 17 | 5. 一定回数のサブステップを実行しても十分な回答が得られない場合は、現時点での最良の回答を出力して終了します。 18 | 19 | 現在の日付:{} 20 | 21 | 常に回答は適切なツールを呼び出して生成してください。 -------------------------------------------------------------------------------- /11/prompts/report_writer_system.prompt: -------------------------------------------------------------------------------- 1 | あなたは文脈を分析し、その中に隠された重要な洞察を引き出す洞察力のあるレポーターです。 2 | 3 | 与えられた情報を使用して、詳細なレポートを作成してください。レポートは、クエリの答えに焦点を当て、構造化され、有益で、詳細かつ包括的で、事実と数字を含むものである必要があります。 4 | 5 | - 偏見のない、ジャーナリスティックな語り口を使用してください。 6 | - 与えられた情報に基づいて、具体的で実行可能かつ妥当な意見を自ら決定しなければなりません。一般的で無意味な結論に流されてはいけません。 7 | - レポートの最後には、参照した情報源のタイトルとURLをすべて参考文献として記載し、重複した情報源は追加せず、それぞれ1つの参考文献のみを記載するようにしてください。 8 | - 各文章の情報源を必ず明記してください。 9 | - レポートは必ず日本語で作成してください。 10 | 11 | 私のキャリアにとって非常に重要なことなので、どうかベストを尽くしてください。 -------------------------------------------------------------------------------- /11/prompts/sufficiency_classifier_system.prompt: -------------------------------------------------------------------------------- 1 | ユーザーからの要求と、生成された中間回答を読んで、中間回答が質問に十分に答えられているかを判定してください。 2 | 3 | 判定基準: 4 | - 中間回答が質問の要点を十分にカバーしている 5 | - 中間回答に質問に関連する重要な情報が含まれている 6 | - 中間回答が質問の意図を満たしている 7 | 8 | 判定は、以下の2つのクラスで行ってください。 9 | True: 中間回答が質問に十分に答えられている 10 | False: 中間回答が質問に十分に答えられていない 11 | 12 | 回答は以下のフォーマットで出力してください: 13 | 判定: {{True or False}} 14 | 理由: {{Reasoning}} -------------------------------------------------------------------------------- /11/prompts/summarize_search_system.prompt: -------------------------------------------------------------------------------- 1 | ユーザーから与えられた情報を、可能な限り簡潔かつ情報密度が高くなるように要約してください。 2 | 3 | - 検索結果から得られた重要な事実を漏れなく抽出してください。 4 | - 出典元ならびにURLを必ず含めてください。 5 | - 重要度の低い詳細な情報は省略してかまいません。 6 | - 要約の分量は、検索結果の分量に応じて調整してください。 -------------------------------------------------------------------------------- /11/requirements.txt: -------------------------------------------------------------------------------- 1 | langchain==0.2.5 2 | langgraph==0.0.69 3 | tavily-python 4 | openai 5 | langchain-openai 6 | retry 7 | pydantic-settings -------------------------------------------------------------------------------- /12/.env.sample: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY= 2 | TAVILY_API_KEY= 3 | LANGCHAIN_TRACING_V2= 4 | LANGCHAIN_PROJECT= 5 | LANGCHAIN_API_KEY= -------------------------------------------------------------------------------- /12/README.md: -------------------------------------------------------------------------------- 1 | ## サンプルコードの実行方法 2 | 3 | 以下のコマンドでサンプルコードが保存されているリポジトリをクローン後、 4 | 5 | ``` 6 | $ git clone https://github.com/mahm/softwaredesign-llm-application.git 7 | ``` 8 | 9 | 続けて以下のコマンドを実行し、必要なライブラリのインストールを行って下さい。 10 | 11 | ``` 12 | $ cd softwaredesign-llm-application/12 13 | $ python -m venv .venv 14 | $ source .venv/bin/activate 15 | $ pip install -r requirements.txt # 必要なライブラリのインストール 16 | ``` 17 | 18 | 次に環境変数の設定を行います。まず`.env.sample`ファイルをコピーして`.env`ファイルを作成します。 19 | 20 | ``` 21 | $ cp .env.sample .env 22 | $ vi .env # お好きなエディタで編集してください 23 | ``` 24 | 25 | 続けて.envファイル内の`OPENAI_API_KEY`と`TAVILY_API_KEY`を設定して下さい。`TAVILY_API_KEY`の取得方法については次節で解説します。 26 | 27 | ``` 28 | OPENAI_API_KEY=[発行されたAPIキーを設定します] 29 | TAVILY_API_KEY=[発行されたAPIキーを設定します] 30 | ``` 31 | 32 | ディレクトリ内にはサンプルコードを収録した`arag_agent.py`が保存されています。お手元で動作を確認される際には、上記のセットアップの後に以下のコマンドを実行してご確認ください。`--task`オプションに続けて作成して欲しいレポートの指示を入力すると、レポートを生成します。 33 | 34 | ``` 35 | python arag_agent.py --task 生成AIスタートアップの最新動向について調査してください 36 | ``` 37 | 38 | ## Tavily APIキーの取得方法 39 | 40 | ### Tavilyについて 41 | 42 | Tavilyは、LLMアプリケーションにおけるRAG(Retrieval-Augmented Generation)に特化した検索エンジンです。 43 | 44 | 通常の検索APIはユーザーのクエリに基づいて検索結果を取得しますが、検索の目的に対して無関係な結果が返ってくることがあります。また、単純なURLやスニペットが返されるため、開発者が関連するコンテンツをスクレイピングしたり、不要な情報をフィルタリングしたりする必要があります。 45 | 46 | 一方でTavilyは、一回のAPI呼び出しで20以上のサイトを集約し、独自のAIを利用しタスクやクエリ、目的に最も関連する情報源とコンテンツを評価、フィルタリング、ランク付けすることが可能な仕組みになっています。 47 | 48 | 今回のサンプルコードを実行するためにはTavilyのAPIキーが必要なので、以下の手段でAPIキーを取得してください。 49 | 50 | ### Tavily APIキーの取得 51 | 52 | 1. まず https://tavily.com/ にアクセスし、画面右上の「Try it out」をクリックします。 53 | 2. するとサインイン画面に遷移するので、そのままサインインを進めて下さい。GoogleアカウントかGitHubアカウントを選択することができます。 54 | 3. サインインが完了するとAPIキーが取得できる画面に遷移します。画面中央部の「API Key」欄より、APIキーをコピーしてください。 55 | 4. APIキーを`.env`内の`TAVILY_API_KEY`に設定してください。 56 | 57 | ## LangSmith APIキーの取得方法 58 | 59 | LangSmithはLLM(大規模言語モデル)実行のログ分析ツールです。LLMアプリケーションの実行を詳細に追跡し、デバッグや評価を行うための機能を提供します。詳細については https://docs.smith.langchain.com/ をご確認ください。 60 | 61 | LangSmithによるトレースを有効にするためには、LangSmithにユーザー登録した上で、APIキーを発行する必要があります。以下の手順を参考にAPIキーを取得してください。 62 | 63 | ### LangSmith APIキーの取得 64 | 65 | 1. [サインアップページ](https://smith.langchain.com/)より、LangSmithのユーザー登録を行います。 66 | 2. [設定ページ](https://smith.langchain.com/settings)を開きます。 67 | 3. API keysタブを開き、「Create API Key」をクリックします。するとAPIキーが発行されますので、APIキーをコピーしてください。 68 | 4. APIキーを`.env`内の`LANGCHAIN_API_KEY`に設定してください。 69 | 5. また`LANGCHAIN_TRACING_V2`に`true`、`LANGCHAIN_PROJECT`に`sd-12`と設定してください。 -------------------------------------------------------------------------------- /12/arag_agent.py: -------------------------------------------------------------------------------- 1 | from operator import add 2 | from typing import Annotated, Any, Literal, Optional 3 | 4 | from langchain_core.output_parsers import StrOutputParser 5 | from langchain_core.prompts import ChatPromptTemplate 6 | from langchain_core.pydantic_v1 import BaseModel, Field 7 | from langchain_openai import ChatOpenAI 8 | from langgraph.graph import END, StateGraph 9 | from multi_step_approach import create_multi_step_agent 10 | from single_step_approach import create_single_step_agent 11 | from utility import load_prompt, run_invoke_agent 12 | 13 | 14 | class Artifact(BaseModel): 15 | action: str = Field(..., description="実行されたアクション") 16 | content: str = Field(..., description="アクションの結果") 17 | 18 | 19 | class AgentState(BaseModel): 20 | task: str = Field(..., description="ユーザーが入力したタスク") 21 | method: Literal["A", "B", "C"] = Field( 22 | default="A", description="選択された方法" 23 | ) 24 | artifacts: Annotated[list[Artifact], add] = Field( 25 | default_factory=list, description="生成された成果物のリスト" 26 | ) 27 | 28 | 29 | class AdaptiveRagAgent: 30 | def __init__(self, llm: ChatOpenAI): 31 | self.llm = llm 32 | self.graph = self._create_graph() 33 | 34 | def _create_graph(self) -> StateGraph: 35 | """グラフを作成し、ノードとエッジを設定する""" 36 | graph = StateGraph(AgentState) 37 | 38 | # ノードの追加 39 | graph.add_node("method_classifier", self._run_method_classifier) 40 | graph.add_node("non_retrieval_qa", self._run_non_retrieval_qa) 41 | graph.add_node("single_step_approach", self._run_single_step_approach) 42 | graph.add_node("multi_step_approach", self._run_multi_step_approach) 43 | 44 | # エッジの設定 45 | graph.set_entry_point("method_classifier") 46 | graph.add_conditional_edges( 47 | "method_classifier", 48 | lambda state: state.method, 49 | { 50 | "A": "non_retrieval_qa", 51 | "B": "single_step_approach", 52 | "C": "multi_step_approach", 53 | }, 54 | ) 55 | graph.add_edge("non_retrieval_qa", END) 56 | graph.add_edge("single_step_approach", END) 57 | graph.add_edge("multi_step_approach", END) 58 | 59 | return graph.compile() 60 | 61 | def _run_method_classifier(self, state: AgentState) -> dict[str, Any]: 62 | """メソッド分類器を実行する""" 63 | prompt = ChatPromptTemplate.from_messages( 64 | [("system", load_prompt("method_classifier_system")), ("user", "{query}")] 65 | ) 66 | chain = prompt | self.llm | StrOutputParser() 67 | method = chain.invoke({"query": state.task}) 68 | return {"method": method} 69 | 70 | def _run_non_retrieval_qa(self, state: AgentState) -> dict[str, Any]: 71 | """非検索型QAを実行する""" 72 | prompt = ChatPromptTemplate.from_messages( 73 | [("system", load_prompt("non_retrieval_qa_system")), ("user", "{query}")] 74 | ) 75 | chain = prompt | self.llm | StrOutputParser() 76 | result = chain.invoke({"query": state.task}) 77 | artifact = Artifact(action="non_retrieval_qa", content=result) 78 | return {"artifacts": [artifact]} 79 | 80 | def _run_single_step_approach(self, state: AgentState) -> dict[str, Any]: 81 | """単一ステップアプローチを実行する""" 82 | inputs = {"messages": [("user", state.task)]} 83 | agent = create_single_step_agent(llm=self.llm) 84 | content = run_invoke_agent(agent, inputs) 85 | artifact = Artifact(action="single_step_approach", content=content) 86 | return {"artifacts": [artifact]} 87 | 88 | def _run_multi_step_approach(self, state: AgentState) -> dict[str, Any]: 89 | """複数ステップアプローチを実行する""" 90 | inputs = {"messages": [("user", state.task)]} 91 | agent = create_multi_step_agent(llm=self.llm) 92 | content = run_invoke_agent(agent, inputs) 93 | artifact = Artifact(action="multi_step_approach", content=content) 94 | return {"artifacts": [artifact]} 95 | 96 | def stream(self, task: str) -> str: 97 | """リサーチタスクを実行し、最終的な状態を返す""" 98 | initial_state = AgentState(task=task) 99 | final_output = "" 100 | for s in self.graph.stream(initial_state, stream_mode="values"): 101 | print(s) 102 | if s["artifacts"]: 103 | latest_artifact = s["artifacts"][-1] 104 | final_output = latest_artifact.content 105 | 106 | return final_output 107 | 108 | 109 | def main(): 110 | import argparse 111 | 112 | from settings import Settings 113 | 114 | # 共通の設定情報を読み込み 115 | settings = Settings() 116 | 117 | # コマンドライン引数のパーサーを作成 118 | parser = argparse.ArgumentParser(description="リサーチタスクを実行します") 119 | parser.add_argument("--task", type=str, required=True, help="実行するタスク") 120 | args = parser.parse_args() 121 | 122 | llm = ChatOpenAI(model=settings.LLM_MODEL_NAME, temperature=0.0) 123 | research_graph = AdaptiveRagAgent(llm=llm) 124 | research_graph.stream(args.task) 125 | 126 | 127 | if __name__ == "__main__": 128 | main() 129 | -------------------------------------------------------------------------------- /12/multi_step_approach.py: -------------------------------------------------------------------------------- 1 | from langchain_openai import ChatOpenAI 2 | from langgraph.prebuilt import create_react_agent 3 | from tools import report_writer, search, sufficiency_check 4 | from utility import load_prompt, run_streaming_agent 5 | 6 | 7 | def create_multi_step_agent(llm: ChatOpenAI): 8 | tools = [search, sufficiency_check, report_writer] 9 | return create_react_agent( 10 | llm, tools=tools, messages_modifier=load_prompt("multi_step_answering_system") 11 | ) 12 | 13 | 14 | def main(): 15 | import argparse 16 | 17 | from settings import Settings 18 | 19 | settings = Settings() 20 | 21 | # コマンドライン引数のパーサーを作成 22 | parser = argparse.ArgumentParser(description="Process some queries.") 23 | parser.add_argument("--task", type=str, required=True, help="The query to search") 24 | 25 | # コマンドライン引数を解析 26 | args = parser.parse_args() 27 | 28 | llm = ChatOpenAI(model=settings.LLM_MODEL_NAME, temperature=0.0) 29 | inputs = {"messages": [("user", args.task)]} 30 | agent = create_multi_step_agent(llm=llm) 31 | final_output = run_streaming_agent(agent, inputs) 32 | print("\n\n") 33 | print("=== final_output ===") 34 | print(final_output) 35 | 36 | 37 | if __name__ == "__main__": 38 | main() 39 | -------------------------------------------------------------------------------- /12/prompts/method_classifier_system.prompt: -------------------------------------------------------------------------------- 1 | ユーザーからの依頼を、以下の3つのクラスに分類してください。 2 | 3 | クラス: 4 | A: Non Retrievalで回答可能な簡単な質問 5 | - 一般的な知識や常識で回答できる質問 6 | - 質問の意図が明確で、簡潔に答えられる質問 7 | B: Single-step Approachで回答可能な中程度の複雑さの質問 8 | - ある特定の情報源(文書や知識ベース)を参照すれば回答できる質問 9 | - 質問の意図は明確だが、外部情報を必要とする質問 10 | C: Multi-step Approachを必要とする複雑な質問 11 | - 複数の情報源を参照し、それらを統合する必要がある質問 12 | - 質問に答えるために、複数の推論ステップや論理的な思考が必要な質問 13 | - 質問の意図が明確でなく、clarificationが必要な質問 14 | 15 | 回答は、クラスラベル(A, B, C)のみを1行で出力してください。 -------------------------------------------------------------------------------- /12/prompts/multi_step_answering_system.prompt: -------------------------------------------------------------------------------- 1 | ユーザーからの質問に、以下の手順で回答してください。 2 | 3 | 手順: 4 | 1. 質問に関連する情報を、指定された情報源から多面的に検索します。 5 | 2. 検索結果を要約し、質問に対する回答を生成します。 6 | 3. 検証用のツールを呼び出し、生成した回答が質問に十分に答えているかを判断します。 7 | - 十分に答えている場合は、回答を出力して終了します。 8 | - 十分に答えていない場合は、追加の情報が必要だと判断します。 9 | 4. 追加の情報が必要な場合は、以下のサブステップを実行します。 10 | a. 現在の検索結果と回答を踏まえ、追加で必要な情報を特定します。 11 | b. 特定した情報を、指定された情報源から再度多面的に検索します。 12 | c. 新しい検索結果を、既存の検索結果と統合します。 13 | d. 統合した情報を基に、質問に対する回答を更新します。 14 | e. 検証用のツールを呼び出し、更新した回答が質問に十分に答えているかを判断します。 15 | - 十分に答えている場合は、回答を出力して終了します。 16 | - 十分に答えていない場合は、ステップ4を繰り返します。 17 | 5. 一定回数のサブステップを実行しても十分な回答が得られない場合は、現時点での最良の回答を出力して終了します。 18 | 19 | 常に回答は適切なツールを呼び出して生成してください。 -------------------------------------------------------------------------------- /12/prompts/non_retrieval_qa_system.prompt: -------------------------------------------------------------------------------- 1 | ユーザーからの質問に、あなたの知識ベースを使って回答してください。回答は事実に基づいたものに限定し、想像や創造による内容は含めないでください。もし質問に対する事実に基づく回答が知識ベース内に見つからない場合は、「わかりません」と答えてください。 -------------------------------------------------------------------------------- /12/prompts/report_writer_system.prompt: -------------------------------------------------------------------------------- 1 | あなたは文脈を分析し、その中に隠された重要な洞察を引き出す洞察力のあるレポーターです。 2 | 3 | ### タスク 4 | 与えられた情報を使用して、詳細なレポートを作成してください。レポートは、クエリの答えに焦点を当て、構造化され、有益で、詳細かつ包括的で、事実と数字を含むものである必要があります。 5 | 6 | - 偏見のない、ジャーナリスティックな語り口を使用してください。 7 | - 与えられた情報に基づいて、具体的で実行可能かつ妥当な意見を自ら決定しなければなりません。一般的で無意味な結論に流されてはいけません。 8 | - レポートの最後には、参照した情報源のタイトルとURLをすべて参考文献として記載し、重複した情報源は追加せず、それぞれ1つの参考文献のみを記載するようにしてください。 9 | - 各文章の情報源を必ず明記してください。 10 | - レポートは必ず日本語で作成してください。 11 | 12 | 私のキャリアにとって非常に重要なことなので、どうかベストを尽くしてください。 -------------------------------------------------------------------------------- /12/prompts/single_step_answering_system.prompt: -------------------------------------------------------------------------------- 1 | ユーザーからの質問に、以下の手順で回答してください。 2 | 3 | 手順: 4 | 1. 質問に関連する情報を、指定された情報源から多面的に検索します。 5 | 2. 検索結果の中から、質問に対して最も関連性の高い情報を選択します。 6 | 3. 選択した情報を要約し、質問に対する回答を生成します。 7 | 8 | 常に回答は適切なツールを呼び出して生成してください。 9 | -------------------------------------------------------------------------------- /12/prompts/sufficiency_classifier_system.prompt: -------------------------------------------------------------------------------- 1 | ユーザーからの要求と、生成された中間回答を読んで、中間回答が質問に十分に答えられているかを判定してください。 2 | 3 | 判定基準: 4 | - 中間回答が質問の要点を十分にカバーしている 5 | - 中間回答に質問に関連する重要な情報が含まれている 6 | - 中間回答が質問の意図を満たしている 7 | 8 | 判定は、以下の2つのクラスで行ってください。 9 | True: 中間回答が質問に十分に答えられている 10 | False: 中間回答が質問に十分に答えられていない 11 | 12 | 回答は以下のフォーマットで出力してください: 13 | 判定: {{True or False}} 14 | 理由: {{Reasoning}} -------------------------------------------------------------------------------- /12/prompts/summarize_search_system.prompt: -------------------------------------------------------------------------------- 1 | ユーザーから与えられた情報を、可能な限り簡潔かつ情報密度が高くなるように要約してください。 2 | 3 | - 検索結果から得られた重要な事実を漏れなく抽出し、その出典元ならびにURLを必ず含めてください。 4 | - 重要度の低い詳細な情報は省略してかまいません。 5 | - 要約の分量は、検索結果の分量に応じて調整してください。 -------------------------------------------------------------------------------- /12/requirements.txt: -------------------------------------------------------------------------------- 1 | langchain==0.2.6 2 | langgraph==0.1.4 3 | tavily-python 4 | openai 5 | langchain-openai 6 | retry 7 | pydantic-settings -------------------------------------------------------------------------------- /12/settings.py: -------------------------------------------------------------------------------- 1 | from pydantic_settings import BaseSettings, SettingsConfigDict 2 | 3 | 4 | class Settings(BaseSettings): 5 | # .envから環境変数を読み込む 6 | model_config = SettingsConfigDict(env_file=".env") 7 | 8 | OPENAI_API_KEY: str 9 | TAVILY_API_KEY: str 10 | LANGCHAIN_TRACING_V2: str = "false" 11 | LANGCHAIN_PROJECT: str = "" 12 | LANGCHAIN_API_KEY: str = "" 13 | 14 | LLM_MODEL_NAME: str = "gpt-4o-2024-05-13" 15 | FAST_LLM_MODEL_NAME: str = "gpt-4o-mini-2024-07-18" 16 | TAVILY_MAX_RESULTS: int = 5 17 | -------------------------------------------------------------------------------- /12/single_step_approach.py: -------------------------------------------------------------------------------- 1 | from langchain_openai import ChatOpenAI 2 | from langgraph.prebuilt import create_react_agent 3 | from tools import report_writer, search 4 | from utility import load_prompt, run_streaming_agent 5 | 6 | 7 | def create_single_step_agent(llm: ChatOpenAI): 8 | tools = [search, report_writer] 9 | return create_react_agent( 10 | llm, tools=tools, messages_modifier=load_prompt("single_step_answering_system") 11 | ) 12 | 13 | 14 | def main(): 15 | import argparse 16 | 17 | from settings import Settings 18 | 19 | # 共通の設定情報を読み込み 20 | settings = Settings() 21 | 22 | # コマンドライン引数のパーサーを作成 23 | parser = argparse.ArgumentParser(description="Process some queries.") 24 | parser.add_argument("--task", type=str, required=True, help="The query to search") 25 | 26 | # コマンドライン引数を解析 27 | args = parser.parse_args() 28 | 29 | llm = ChatOpenAI(model=settings.LLM_MODEL_NAME, temperature=0.0) 30 | inputs = {"messages": [("user", args.task)]} 31 | agent = create_single_step_agent(llm=llm) 32 | final_output = run_streaming_agent(agent, inputs) 33 | print("\n\n") 34 | print("=== final_output ===") 35 | print(final_output) 36 | 37 | 38 | if __name__ == "__main__": 39 | main() 40 | -------------------------------------------------------------------------------- /12/tools.py: -------------------------------------------------------------------------------- 1 | import os 2 | from functools import lru_cache 3 | 4 | from langchain_core.output_parsers import StrOutputParser 5 | from langchain_core.prompts import ChatPromptTemplate 6 | from langchain_core.runnables import Runnable 7 | from langchain_core.tools import tool 8 | from langchain_openai import ChatOpenAI 9 | from settings import Settings 10 | from tavily import TavilyClient 11 | from utility import load_prompt 12 | 13 | TAVILY_MAX_RESULTS = 5 14 | 15 | settings = Settings() 16 | 17 | 18 | # Tavilyクライアントのインスタンスを取得する関数 19 | @lru_cache 20 | def tavily_client() -> TavilyClient: 21 | return TavilyClient(api_key=os.environ["TAVILY_API_KEY"]) 22 | 23 | 24 | def summarize_search_chain() -> Runnable: 25 | llm = ChatOpenAI(model=settings.FAST_LLM_MODEL_NAME, temperature=0.0) 26 | prompt = ChatPromptTemplate.from_messages( 27 | [("system", "{system}"), ("user", "{query}")] 28 | ).partial( 29 | system=load_prompt("summarize_search_system"), 30 | ) 31 | return prompt | llm | StrOutputParser() 32 | 33 | 34 | # 検索を実行するツール 35 | @tool 36 | def search(query: str) -> str: 37 | """Search for the given query and return the results as a string.""" 38 | response = tavily_client().search( 39 | query, max_results=TAVILY_MAX_RESULTS, include_raw_content=True 40 | ) 41 | queries = [] 42 | for document in response["results"]: 43 | # 1万字以上のコンテンツは省略 44 | if len(document["raw_content"]) > 10000: 45 | document["raw_content"] = document["raw_content"][:10000] + "..." 46 | queries.append( 47 | { 48 | "query": f"\ntitle: {document['title']}\nurl: {document['url']}\ncontent: {document['raw_content']}\n" 49 | } 50 | ) 51 | summarize_results = summarize_search_chain().batch(queries) 52 | xml_results = [] 53 | for result in summarize_results: 54 | xml_result = "{}".format(result) 55 | xml_results.append(xml_result) 56 | return "\n\n".join(xml_results) 57 | 58 | 59 | # 成果物の内容がユーザー要求に対して十分かどうかをチェックするツール 60 | @tool 61 | def sufficiency_check(user_requirement: str, result: str) -> str: 62 | """Determine whether the answers generated adequately answer the question.""" 63 | llm = ChatOpenAI(model=settings.LLM_MODEL_NAME, temperature=0.0) 64 | user_prompt = f"ユーザーからの要求: {user_requirement}\n生成結果: {result}\n十分かどうかを判断してください。" 65 | prompt = ChatPromptTemplate.from_messages( 66 | [("system", "{system}"), ("user", "{user}")] 67 | ).partial( 68 | system=load_prompt("sufficiency_classifier_system"), 69 | user=user_prompt, 70 | ) 71 | chain = prompt | llm | StrOutputParser() 72 | return chain.invoke({}) 73 | 74 | 75 | # レポートを生成するツール 76 | @tool 77 | def report_writer(user_requirement: str, source: str) -> str: 78 | """Generate reports based on user requests and sources of information gathered through searches.""" 79 | llm = ChatOpenAI(model=settings.LLM_MODEL_NAME, temperature=0.0) 80 | user_prompt = f"ユーザーからの要求: {user_requirement}\n情報源: {source}\n必ず情報源を基にユーザーからの要求を満たすレポートを生成してください。" 81 | prompt = ChatPromptTemplate.from_messages( 82 | [("system", "{system}"), ("user", "{user}")] 83 | ).partial( 84 | system=load_prompt("report_writer_system"), 85 | user=user_prompt, 86 | ) 87 | chain = prompt | llm | StrOutputParser() 88 | return chain.invoke({}) 89 | -------------------------------------------------------------------------------- /12/utility.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Any 3 | 4 | from langgraph.graph.graph import CompiledGraph 5 | 6 | 7 | # プロンプトファイルを読み込む関数 8 | def load_prompt(name: str) -> str: 9 | prompt_path = os.path.join(os.path.dirname(__file__), "prompts", f"{name}.prompt") 10 | with open(prompt_path, "r") as f: 11 | return f.read() 12 | 13 | 14 | # stream関数でエージェントを呼び出し、実行結果を逐次表示する 15 | def run_streaming_agent( 16 | agent: CompiledGraph, inputs: list[dict[str, Any]], verbose: bool = False 17 | ) -> str: 18 | result = "" 19 | for s in agent.stream(inputs, stream_mode="values"): 20 | message = s["messages"][-1] 21 | if isinstance(message, tuple): 22 | result = str(message) 23 | else: 24 | message.pretty_print() 25 | result = message.content 26 | 27 | if verbose: 28 | display_result = "" 29 | if len(result) > 1000: 30 | display_result = result[:1000] + "..." 31 | else: 32 | display_result = result 33 | print(display_result) 34 | return result 35 | 36 | 37 | # invoke関数でエージェントを呼び出し、実行結果を返す 38 | def run_invoke_agent(agent: CompiledGraph, inputs: list[dict[str, Any]]) -> str: 39 | result = agent.invoke(inputs) 40 | return str(result["messages"][-1]) 41 | -------------------------------------------------------------------------------- /14/.env.sample: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY= 2 | TAVILY_API_KEY= 3 | LANGCHAIN_TRACING_V2= 4 | LANGCHAIN_PROJECT= 5 | LANGCHAIN_API_KEY= -------------------------------------------------------------------------------- /14/.tool-versions: -------------------------------------------------------------------------------- 1 | python 3.11.8 2 | -------------------------------------------------------------------------------- /14/README.md: -------------------------------------------------------------------------------- 1 | ※ 第14回、第15回のサンプルコードは同じものを使用しています。 2 | 3 | ## サンプルコードの実行方法 4 | 5 | 以下のコマンドでサンプルコードが保存されているリポジトリをクローン後、 6 | 7 | ``` 8 | $ git clone https://github.com/mahm/softwaredesign-llm-application.git 9 | ``` 10 | 11 | 続けて以下のコマンドを実行し、必要なライブラリのインストールを行って下さい。 12 | 13 | ``` 14 | $ cd softwaredesign-llm-application/14 15 | $ pip install poetry 16 | $ poetry install # 必要なライブラリのインストール 17 | ``` 18 | 19 | 次に環境変数の設定を行います。まず`.env.sample`ファイルをコピーして`.env`ファイルを作成します。 20 | 21 | ``` 22 | $ cp .env.sample .env 23 | $ vi .env # お好きなエディタで編集してください 24 | ``` 25 | 26 | 続けて.envファイル内の`OPENAI_API_KEY`と`TAVILY_API_KEY`を設定して下さい。`TAVILY_API_KEY`の取得方法については次節で解説します。 27 | 28 | ``` 29 | OPENAI_API_KEY=[発行されたAPIキーを設定します] 30 | TAVILY_API_KEY=[発行されたAPIキーを設定します] 31 | ``` 32 | 33 | お手元で動作を確認される際には、上記のセットアップの後に以下のコマンドを実行してご確認ください。 34 | 35 | ``` 36 | $ poetry run streamlit run app.py 37 | ``` 38 | 39 | ## Tavily APIキーの取得方法 40 | 41 | ### Tavilyについて 42 | 43 | Tavilyは、LLMアプリケーションにおけるRAG(Retrieval-Augmented Generation)に特化した検索エンジンです。 44 | 45 | 通常の検索APIはユーザーのクエリに基づいて検索結果を取得しますが、検索の目的に対して無関係な結果が返ってくることがあります。また、単純なURLやスニペットが返されるため、開発者が関連するコンテンツをスクレイピングしたり、不要な情報をフィルタリングしたりする必要があります。 46 | 47 | 一方でTavilyは、一回のAPI呼び出しで20以上のサイトを集約し、独自のAIを利用しタスクやクエリ、目的に最も関連する情報源とコンテンツを評価、フィルタリング、ランク付けすることが可能な仕組みになっています。 48 | 49 | 今回のサンプルコードを実行するためにはTavilyのAPIキーが必要なので、以下の手段でAPIキーを取得してください。 50 | 51 | ### Tavily APIキーの取得 52 | 53 | 1. まず https://tavily.com/ にアクセスし、画面右上の「Try it out」をクリックします。 54 | 2. するとサインイン画面に遷移するので、そのままサインインを進めて下さい。GoogleアカウントかGitHubアカウントを選択することができます。 55 | 3. サインインが完了するとAPIキーが取得できる画面に遷移します。画面中央部の「API Key」欄より、APIキーをコピーしてください。 56 | 4. APIキーを`.env`内の`TAVILY_API_KEY`に設定してください。 57 | 58 | ## LangSmith APIキーの取得方法 59 | 60 | LangSmithはLLM(大規模言語モデル)実行のログ分析ツールです。LLMアプリケーションの実行を詳細に追跡し、デバッグや評価を行うための機能を提供します。詳細については https://docs.smith.langchain.com/ をご確認ください。 61 | 62 | LangSmithによるトレースを有効にするためには、LangSmithにユーザー登録した上で、APIキーを発行する必要があります。以下の手順を参考にAPIキーを取得してください。 63 | 64 | ### LangSmith APIキーの取得 65 | 66 | 1. [サインアップページ](https://smith.langchain.com/)より、LangSmithのユーザー登録を行います。 67 | 2. [設定ページ](https://smith.langchain.com/settings)を開きます。 68 | 3. API keysタブを開き、「Create API Key」をクリックします。するとAPIキーが発行されますので、APIキーをコピーしてください。 69 | 4. APIキーを`.env`内の`LANGCHAIN_API_KEY`に設定してください。 70 | 5. また`LANGCHAIN_TRACING_V2`に`true`、`LANGCHAIN_PROJECT`に`sd-12`と設定してください。 -------------------------------------------------------------------------------- /14/app.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Literal 2 | from uuid import uuid4 3 | 4 | import streamlit as st 5 | from agent import HumanInTheLoopAgent 6 | from dotenv import load_dotenv 7 | from langchain_openai import ChatOpenAI 8 | 9 | 10 | def show_message(type: Literal["human", "agent"], title: str, message: str) -> None: 11 | with st.chat_message(type): 12 | st.markdown(f"**{title}**") 13 | st.markdown(message) 14 | 15 | 16 | def app() -> None: 17 | load_dotenv(override=True) 18 | 19 | st.title("Human-in-the-loopを適用したリサーチエージェント") 20 | 21 | # st.session_stateにagentを保存 22 | if "agent" not in st.session_state: 23 | _llm = ChatOpenAI(model="gpt-4o", temperature=0.5) 24 | _agent = HumanInTheLoopAgent(_llm) 25 | _agent.subscribe(show_message) 26 | st.session_state.agent = _agent 27 | 28 | agent = st.session_state.agent 29 | 30 | # グラフを表示 31 | with st.sidebar: 32 | st.image(agent.mermaid_png()) 33 | 34 | # st.session_stateにthread_idを保存 35 | if "thread_id" not in st.session_state: 36 | st.session_state.thread_id = uuid4().hex 37 | thread_id = st.session_state.thread_id 38 | st.write(f"thread_id: {thread_id}") 39 | 40 | # ユーザーの入力を受け付ける 41 | human_message = st.chat_input() 42 | if human_message: 43 | with st.spinner(): 44 | agent.handle_human_message(human_message, thread_id) 45 | # ユーザー入力があった場合、承認状態をリセット 46 | st.session_state.approval_state = "pending" 47 | 48 | # 次がhuman_approvalの場合は承認ボタンを表示 49 | if agent.is_next_human_approval_node(thread_id): 50 | if "approval_state" not in st.session_state: 51 | st.session_state.approval_state = "pending" 52 | 53 | if st.session_state.approval_state == "pending": 54 | approved = st.button("承認") 55 | if approved: 56 | st.session_state.approval_state = "processing" 57 | st.rerun() 58 | elif st.session_state.approval_state == "processing": 59 | with st.spinner("タスク処理中..."): 60 | agent.handle_human_message("[APPROVE]", thread_id) 61 | st.session_state.approval_state = "pending" 62 | 63 | 64 | app() 65 | -------------------------------------------------------------------------------- /14/poetry.toml: -------------------------------------------------------------------------------- 1 | [virtualenvs] 2 | in-project = true 3 | prefer-active-python = true -------------------------------------------------------------------------------- /14/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "sd14-sample-code" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["Masahiro Nishimi <498686+mahm@users.noreply.github.com>"] 6 | readme = "README.md" 7 | package-mode = false 8 | 9 | [tool.poetry.dependencies] 10 | python = "^3.11" 11 | langgraph = "^0.2.21" 12 | langchain-openai = "^0.2.0" 13 | python-dotenv = "^1.0.1" 14 | streamlit = "^1.38.0" 15 | langgraph-checkpoint = "^1.0.9" 16 | langchain-community = "^0.3.0" 17 | 18 | 19 | [build-system] 20 | requires = ["poetry-core"] 21 | build-backend = "poetry.core.masonry.api" 22 | 23 | [tool.mypy] 24 | strict = true 25 | 26 | [tool.ruff.lint] 27 | select = ["ALL"] 28 | ignore = ["D", "ANN", "TRY", "EM", "FA", "SIM108", "S101", "RET505", "TCH002"] -------------------------------------------------------------------------------- /16/.env.sample: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY= 2 | ANTHROPIC_API_KEY= 3 | TAVILY_API_KEY= 4 | LANGSMITH_TRACING_V2= 5 | LANGSMITH_API_KEY= 6 | LANGSMITH_PROJECT= -------------------------------------------------------------------------------- /16/.gitignore: -------------------------------------------------------------------------------- 1 | # Python-generated files 2 | __pycache__/ 3 | *.py[oc] 4 | build/ 5 | dist/ 6 | wheels/ 7 | *.egg-info 8 | 9 | # Virtual environments 10 | .venv 11 | .env 12 | 13 | # data files 14 | data/ -------------------------------------------------------------------------------- /16/.python-version: -------------------------------------------------------------------------------- 1 | 3.11 2 | -------------------------------------------------------------------------------- /16/README.md: -------------------------------------------------------------------------------- 1 | # Software Design誌「実践LLMアプリケーション開発」第16回サンプルコード 2 | 3 | ## サンプルコードの実行方法 4 | 5 | ※ このプロジェクトは`uv`を使用しています。`uv`のインストール方法については[こちら](https://github.com/astral-sh/uv)をご確認ください。 6 | 7 | 以下のコマンドでサンプルコードが保存されているリポジトリをクローン後、 8 | 9 | ``` 10 | $ git clone https://github.com/mahm/sd-16.git 11 | ``` 12 | 13 | 続けて以下のコマンドを実行し、必要なライブラリのインストールを行って下さい。 14 | 15 | ``` 16 | $ cd sd-16 17 | $ uv sync 18 | ``` 19 | 20 | 次に環境変数の設定を行います。まず`.env.sample`ファイルをコピーして`.env`ファイルを作成します。 21 | 22 | ``` 23 | $ cp .env.sample .env 24 | $ vi .env # お好きなエディタで編集してください 25 | ``` 26 | 27 | 続けて.envファイル内の以下のキーを設定して下さい。`TAVILY_API_KEY`ならびに`LANGSMITH_API_KEY`の取得方法については次節で解説します。 28 | 29 | ``` 30 | OPENAI_API_KEY=[発行されたAPIキーを設定します] 31 | ANTHROPIC_API_KEY=[発行されたAPIキーを設定します] 32 | TAVILY_API_KEY=[発行されたAPIキーを設定します] 33 | LANGSMITH_TRACING_V2=true 34 | LANGSMITH_API_KEY=[発行されたAPIキーを設定します] 35 | LANGSMITH_PROJECT=sd-16 36 | ``` 37 | 38 | お手元で動作を確認される際には、上記のセットアップの後に以下のコマンドを実行してご確認ください。 39 | 40 | ``` 41 | $ pip install -U langgraph-cli 42 | $ langgraph up 43 | ``` 44 | 45 | ## Tavily APIキーの取得方法 46 | 47 | ### Tavilyについて 48 | 49 | Tavilyは、LLMアプリケーションにおけるRAG(Retrieval-Augmented Generation)に特化した検索エンジンです。 50 | 51 | 通常の検索APIはユーザーのクエリに基づいて検索結果を取得しますが、検索の目的に対して無関係な結果が返ってくることがあります。また、単純なURLやスニペットが返されるため、開発者が関連するコンテンツをスクレイピングしたり、不要な情報をフィルタリングしたりする必要があります。 52 | 53 | 一方でTavilyは、一回のAPI呼び出しで20以上のサイトを集約し、独自のAIを利用しタスクやクエリ、目的に最も関連する情報源とコンテンツを評価、フィルタリング、ランク付けすることが可能な仕組みになっています。 54 | 55 | 今回のサンプルコードを実行するためにはTavilyのAPIキーが必要なので、以下の手段でAPIキーを取得してください。 56 | 57 | ### Tavily APIキーの取得 58 | 59 | 1. まず https://tavily.com/ にアクセスし、画面右上の「Try it out」をクリックします。 60 | 2. するとサインイン画面に遷移するので、そのままサインインを進めて下さい。GoogleアカウントかGitHubアカウントを選択することができます。 61 | 3. サインインが完了するとAPIキーが取得できる画面に遷移します。画面中央部の「API Key」欄より、APIキーをコピーしてください。 62 | 4. APIキーを`.env`内の`TAVILY_API_KEY`に設定してください。 63 | 64 | ## LangSmith APIキーの取得方法 65 | 66 | LangSmithはLLM(大規模言語モデル)実行のログ分析ツールです。LLMアプリケーションの実行を詳細に追跡し、デバッグや評価を行うための機能を提供します。詳細については https://docs.smith.langchain.com/ をご確認ください。 67 | 68 | LangSmithによるトレースを有効にするためには、LangSmithにユーザー登録した上で、APIキーを発行する必要があります。以下の手順を参考にAPIキーを取得してください。 69 | 70 | ### LangSmith APIキーの取得 71 | 72 | 1. [サインアップページ](https://smith.langchain.com/)より、LangSmithのユーザー登録を行います。 73 | 2. [設定ページ](https://smith.langchain.com/settings)を開きます。 74 | 3. API keysタブを開き、「Create API Key」をクリックします。するとAPIキーが発行されますので、APIキーをコピーしてください。 75 | 4. APIキーを`.env`内の`LANGCHAIN_API_KEY`に設定してください。 76 | -------------------------------------------------------------------------------- /16/langgraph.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": ["."], 3 | "graphs": { 4 | "agent": "./my_agent/agent.py:graph" 5 | }, 6 | "env": ".env" 7 | } 8 | -------------------------------------------------------------------------------- /16/my_agent/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahm/softwaredesign-llm-application/a2ab09fde6c62bd538d4170ecc319000949ebfa0/16/my_agent/__init__.py -------------------------------------------------------------------------------- /16/my_agent/agent.py: -------------------------------------------------------------------------------- 1 | from functools import lru_cache 2 | from typing import Annotated, ClassVar, Literal, Sequence 3 | 4 | from langchain_anthropic import ChatAnthropic 5 | from langchain_community.tools.tavily_search import TavilySearchResults 6 | from langchain_core.messages import BaseMessage 7 | from langchain_openai import ChatOpenAI 8 | from langgraph.graph import END, START, StateGraph, add_messages 9 | from langgraph.prebuilt import ToolNode 10 | from pydantic import BaseModel 11 | 12 | 13 | # グラフのステートを定義 14 | class AgentState(BaseModel): 15 | messages: Annotated[Sequence[BaseMessage], add_messages] 16 | 17 | 18 | # グラフの設定を定義 19 | class GraphConfig(BaseModel): 20 | ANTHROPIC: ClassVar[str] = "anthropic" 21 | OPENAI: ClassVar[str] = "openai" 22 | 23 | model_name: Literal["anthropic", "openai"] 24 | 25 | 26 | # モデルを取得する関数 27 | @lru_cache(maxsize=2) 28 | def _get_model(model_name: str) -> ChatOpenAI | ChatAnthropic: 29 | if model_name == GraphConfig.OPENAI: 30 | model = ChatOpenAI(temperature=0, model_name="gpt-4o") 31 | elif model_name == GraphConfig.ANTHROPIC: 32 | model = ChatAnthropic(temperature=0, model_name="claude-3-5-sonnet-20241022") 33 | else: 34 | raise ValueError(f"サポートしていないモデルです: {model_name}") 35 | 36 | model = model.bind_tools(tools) 37 | return model 38 | 39 | 40 | # ツール実行のためのノードを定義 41 | tools = [TavilySearchResults(max_results=1)] 42 | tool_node = ToolNode(tools) 43 | 44 | 45 | # ツール実行の継続条件を定義 46 | def should_continue(state: AgentState) -> Literal["end", "continue"]: 47 | last_message = state.messages[-1] 48 | return "continue" if last_message.tool_calls else "end" 49 | 50 | 51 | # モデルを実行する関数 52 | def call_model(state: AgentState, config: GraphConfig) -> dict: 53 | messages = state.messages 54 | messages = [ 55 | {"role": "system", "content": "You are a helpful assistant."} 56 | ] + messages 57 | # configの情報からモデル名を取得 58 | model_name = config.get("configurable", {}).get("model_name", "anthropic") 59 | # モデル名から利用するモデルのインスタンスを取得 60 | model = _get_model(model_name) 61 | # モデルを実行 62 | response = model.invoke(messages) 63 | return {"messages": [response]} 64 | 65 | 66 | # グラフを定義 67 | workflow = StateGraph(AgentState, config_schema=GraphConfig) 68 | 69 | # ノードを追加 70 | workflow.add_node("call_model", call_model) 71 | workflow.add_node("tool_node", tool_node) 72 | 73 | # エッジを追加 74 | workflow.add_edge(START, "call_model") 75 | workflow.add_conditional_edges( 76 | "call_model", 77 | should_continue, 78 | { 79 | "continue": "tool_node", 80 | "end": END, 81 | }, 82 | ) 83 | workflow.add_edge("tool_node", "call_model") 84 | 85 | graph = workflow.compile() 86 | -------------------------------------------------------------------------------- /16/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "sd-16" 3 | version = "0.1.0" 4 | description = "Add your description here" 5 | readme = "README.md" 6 | requires-python = ">=3.11" 7 | dependencies = [ 8 | "langchain-anthropic>=0.3.0", 9 | "langchain-community>=0.3.7", 10 | "langchain-core>=0.3.19", 11 | "langchain-openai>=0.2.8", 12 | "langgraph>=0.2.50", 13 | "tavily-python>=0.5.0", 14 | ] 15 | 16 | [tool.uv] 17 | dev-dependencies = [ 18 | "langgraph-cli==0.1.56", 19 | ] 20 | -------------------------------------------------------------------------------- /17/.env.sample: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY= 2 | TAVILY_API_KEY= 3 | LANGSMITH_TRACING_V2= 4 | LANGSMITH_API_KEY= 5 | LANGSMITH_PROJECT= -------------------------------------------------------------------------------- /17/.gitignore: -------------------------------------------------------------------------------- 1 | # Python-generated files 2 | __pycache__/ 3 | *.py[oc] 4 | build/ 5 | dist/ 6 | wheels/ 7 | *.egg-info 8 | 9 | # Virtual environments 10 | .venv 11 | .env 12 | 13 | # data files 14 | data/ 15 | 16 | .langgraph_api/ -------------------------------------------------------------------------------- /17/.python-version: -------------------------------------------------------------------------------- 1 | 3.11 2 | -------------------------------------------------------------------------------- /17/README.md: -------------------------------------------------------------------------------- 1 | # Software Design誌「実践LLMアプリケーション開発」第17回サンプルコード 2 | 3 | ## サンプルコードの実行方法 4 | 5 | ※ このプロジェクトは`uv`を使用しています。`uv`のインストール方法については[こちら](https://github.com/astral-sh/uv)をご確認ください。 6 | 7 | 以下のコマンドを実行し、必要なライブラリのインストールを行って下さい。 8 | 9 | ``` 10 | $ uv sync 11 | ``` 12 | 13 | 次に環境変数の設定を行います。まず`.env.sample`ファイルをコピーして`.env`ファイルを作成します。 14 | 15 | ``` 16 | $ cp .env.sample .env 17 | $ vi .env # お好きなエディタで編集してください 18 | ``` 19 | 20 | 続けて.envファイル内の以下のキーを設定して下さい。`TAVILY_API_KEY`ならびに`LANGSMITH_API_KEY`の取得方法については次節で解説します。 21 | 22 | ``` 23 | OPENAI_API_KEY=[発行されたAPIキーを設定します] 24 | TAVILY_API_KEY=[発行されたAPIキーを設定します] 25 | LANGSMITH_TRACING_V2=true 26 | LANGSMITH_API_KEY=[発行されたAPIキーを設定します] 27 | LANGSMITH_PROJECT=sd-17 28 | ``` 29 | 30 | お手元で動作を確認される際には、上記のセットアップの後に以下のコマンドを実行してご確認ください。 31 | 32 | ``` 33 | $ uv run langgraph dev 34 | ``` 35 | 36 | ## Tavily APIキーの取得方法 37 | 38 | ### Tavilyについて 39 | 40 | Tavilyは、LLMアプリケーションにおけるRAG(Retrieval-Augmented Generation)に特化した検索エンジンです。 41 | 42 | 通常の検索APIはユーザーのクエリに基づいて検索結果を取得しますが、検索の目的に対して無関係な結果が返ってくることがあります。また、単純なURLやスニペットが返されるため、開発者が関連するコンテンツをスクレイピングしたり、不要な情報をフィルタリングしたりする必要があります。 43 | 44 | 一方でTavilyは、一回のAPI呼び出しで20以上のサイトを集約し、独自のAIを利用しタスクやクエリ、目的に最も関連する情報源とコンテンツを評価、フィルタリング、ランク付けすることが可能な仕組みになっています。 45 | 46 | 今回のサンプルコードを実行するためにはTavilyのAPIキーが必要なので、以下の手段でAPIキーを取得してください。 47 | 48 | ### Tavily APIキーの取得 49 | 50 | 1. まず https://tavily.com/ にアクセスし、画面右上の「Try it out」をクリックします。 51 | 2. するとサインイン画面に遷移するので、そのままサインインを進めて下さい。GoogleアカウントかGitHubアカウントを選択することができます。 52 | 3. サインインが完了するとAPIキーが取得できる画面に遷移します。画面中央部の「API Key」欄より、APIキーをコピーしてください。 53 | 4. APIキーを`.env`内の`TAVILY_API_KEY`に設定してください。 54 | 55 | ## LangSmith APIキーの取得方法 56 | 57 | LangSmithはLLM(大規模言語モデル)実行のログ分析ツールです。LLMアプリケーションの実行を詳細に追跡し、デバッグや評価を行うための機能を提供します。詳細については https://docs.smith.langchain.com/ をご確認ください。 58 | 59 | LangSmithによるトレースを有効にするためには、LangSmithにユーザー登録した上で、APIキーを発行する必要があります。以下の手順を参考にAPIキーを取得してください。 60 | 61 | ### LangSmith APIキーの取得 62 | 63 | 1. [サインアップページ](https://smith.langchain.com/)より、LangSmithのユーザー登録を行います。 64 | 2. [設定ページ](https://smith.langchain.com/settings)を開きます。 65 | 3. API keysタブを開き、「Create API Key」をクリックします。するとAPIキーが発行されますので、APIキーをコピーしてください。 66 | 4. APIキーを`.env`内の`LANGCHAIN_API_KEY`に設定してください。 67 | -------------------------------------------------------------------------------- /17/langgraph.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": ["."], 3 | "graphs": { 4 | "agent": "./my_agent/agent.py:graph", 5 | "task_planner": "./my_agent/task_planner_agent.py:graph", 6 | "task_executor": "./my_agent/task_executor_agent.py:graph" 7 | }, 8 | "env": ".env" 9 | } 10 | -------------------------------------------------------------------------------- /17/my_agent/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahm/softwaredesign-llm-application/a2ab09fde6c62bd538d4170ecc319000949ebfa0/17/my_agent/__init__.py -------------------------------------------------------------------------------- /17/my_agent/agent.py: -------------------------------------------------------------------------------- 1 | import operator 2 | from datetime import datetime 3 | from typing import Annotated, Callable, Sequence, TypedDict 4 | 5 | from langchain_core.messages import BaseMessage 6 | from langchain_core.output_parsers import StrOutputParser 7 | from langchain_core.prompts import ChatPromptTemplate 8 | from langchain_openai import ChatOpenAI 9 | from langgraph.checkpoint.memory import MemorySaver 10 | from langgraph.graph import END, START, StateGraph, add_messages 11 | from langgraph.graph.state import CompiledStateGraph 12 | from my_agent.task_executor_agent import \ 13 | create_agent as create_task_executor_agent 14 | from my_agent.task_planner_agent import \ 15 | create_agent as create_task_planner_agent 16 | 17 | 18 | class AgentInputState(TypedDict): 19 | messages: Annotated[Sequence[BaseMessage], add_messages] 20 | 21 | 22 | class AgentPrivateState(TypedDict): 23 | tasks: list[str] 24 | results: list[str] 25 | 26 | 27 | class AgentOutputState(TypedDict): 28 | final_output: str 29 | 30 | 31 | class AgentState(AgentInputState, AgentPrivateState, AgentOutputState): 32 | pass 33 | 34 | 35 | class Reporter: 36 | def __init__(self, llm: ChatOpenAI): 37 | self.llm = llm 38 | 39 | def __call__(self, state: AgentPrivateState) -> dict: 40 | return {"final_output": self.run(state.get("results", []))} 41 | 42 | def run(self, results: list[str]) -> str: 43 | prompt = ChatPromptTemplate.from_template( 44 | "### 調査結果\n" 45 | "{results}\n\n" 46 | "### タスク\n" 47 | "調査結果に基づき、調査結果の内容を漏れなく整理したレポートを作成してください。" 48 | ) 49 | results_str = "\n\n".join( 50 | f"Info {i+1}:\n{result}" for i, result in enumerate(results) 51 | ) 52 | chain = prompt | self.llm | StrOutputParser() 53 | return chain.invoke({"results": results_str}) 54 | 55 | 56 | class Agent: 57 | def __init__(self, llm: ChatOpenAI) -> None: 58 | self.llm = llm 59 | self.reporter = Reporter(llm) 60 | self.graph = self._create_graph() 61 | 62 | def _create_graph(self) -> CompiledStateGraph: 63 | graph = StateGraph( 64 | state_schema=AgentState, 65 | input=AgentInputState, 66 | output=AgentOutputState, 67 | ) 68 | 69 | graph.add_node("task_planner", create_task_planner_agent(self.llm)) 70 | graph.add_node("task_executor", create_task_executor_agent(self.llm)) 71 | graph.add_node("reporter", self.reporter) 72 | 73 | graph.add_edge(START, "task_planner") 74 | graph.add_edge("task_planner", "task_executor") 75 | graph.add_edge("task_executor", "reporter") 76 | graph.add_edge("reporter", END) 77 | 78 | return graph.compile(checkpointer=MemorySaver()) 79 | 80 | 81 | def create_agent(llm: ChatOpenAI) -> CompiledStateGraph: 82 | return Agent(llm).graph 83 | 84 | 85 | graph = create_agent(ChatOpenAI(model="gpt-4o-mini")) 86 | 87 | if __name__ == "__main__": 88 | png = graph.get_graph(xray=2).draw_mermaid_png() 89 | with open("graph.png", "wb") as f: 90 | f.write(png) 91 | -------------------------------------------------------------------------------- /17/my_agent/task_executor_agent.py: -------------------------------------------------------------------------------- 1 | import operator 2 | from typing import Annotated, Sequence, TypedDict 3 | 4 | from langchain_community.tools.tavily_search import TavilySearchResults 5 | from langchain_openai import ChatOpenAI 6 | from langgraph.graph import END, START, StateGraph 7 | from langgraph.graph.state import CompiledStateGraph 8 | from langgraph.prebuilt import create_react_agent 9 | from langgraph.types import Send 10 | 11 | 12 | class TaskExecutorAgentInputState(TypedDict): 13 | tasks: list[str] 14 | 15 | 16 | class TaskExecutorAgentOutputState(TypedDict): 17 | results: Annotated[Sequence[str], operator.add] 18 | 19 | 20 | class TaskExecutorAgentState(TaskExecutorAgentInputState, TaskExecutorAgentOutputState): 21 | pass 22 | 23 | 24 | # 並列ノードに値を受け渡すためのState 25 | class ParallelState(TypedDict): 26 | task: str 27 | 28 | 29 | class TaskExecutor: 30 | def __init__(self, llm: ChatOpenAI): 31 | self.llm = llm 32 | self.tools = [TavilySearchResults(max_results=3)] 33 | 34 | def __call__(self, state: ParallelState) -> dict: 35 | return {"results": [self.run(state.get("task", ""))]} 36 | 37 | def run(self, task: str) -> str: 38 | messages = { 39 | "messages": [ 40 | ( 41 | "human", 42 | f"次のタスクを実行し、詳細な回答を提供してください。\n\nタスク: {task}\n\n" 43 | "要件:\n" 44 | "1. 必要に応じて提供されたツールを使用してください。\n" 45 | "2. 実行は徹底的かつ包括的に行ってください。\n" 46 | "3. 可能な限り具体的な事実やデータを提供してください。\n" 47 | "4. 発見した内容を明確に要約してください。\n", 48 | ) 49 | ] 50 | } 51 | agent = create_react_agent(self.llm, self.tools) 52 | result = agent.invoke(messages) 53 | return result["messages"][-1].content 54 | 55 | 56 | class TaskExecutorAgent: 57 | def __init__(self, llm: ChatOpenAI) -> None: 58 | self.llm = llm 59 | self.task_executor = TaskExecutor(self.llm) 60 | self.graph = self._create_graph() 61 | 62 | def _create_graph(self) -> CompiledStateGraph: 63 | graph = StateGraph( 64 | state_schema=TaskExecutorAgentState, 65 | input=TaskExecutorAgentInputState, 66 | output=TaskExecutorAgentOutputState, 67 | ) 68 | graph.add_node("execute_task", self.task_executor) 69 | graph.add_conditional_edges( 70 | START, self.routing_parallel_nodes, ["execute_task"] 71 | ) 72 | graph.add_edge("execute_task", END) 73 | 74 | return graph.compile() 75 | 76 | def routing_parallel_nodes(self, state: TaskExecutorAgentInputState) -> list[Send]: 77 | return [Send("execute_task", {"task": task}) for task in state.get("tasks", [])] 78 | 79 | 80 | def create_agent(llm: ChatOpenAI) -> CompiledStateGraph: 81 | return TaskExecutorAgent(llm).graph 82 | 83 | 84 | graph = create_agent(ChatOpenAI(model="gpt-4o-mini")) 85 | -------------------------------------------------------------------------------- /17/my_agent/task_planner_agent.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Annotated, Sequence, TypedDict 3 | 4 | from langchain_core.messages import BaseMessage 5 | from langchain_core.prompts import ChatPromptTemplate 6 | from langchain_openai import ChatOpenAI 7 | from langgraph.checkpoint.memory import MemorySaver 8 | from langgraph.graph import END, START, StateGraph, add_messages 9 | from langgraph.graph.state import CompiledStateGraph 10 | from pydantic import BaseModel, Field 11 | 12 | 13 | class TaskPlannerAgentInputState(TypedDict): 14 | messages: Annotated[Sequence[BaseMessage], add_messages] 15 | 16 | class TaskPlannerAgentPrivateState(TypedDict): 17 | is_approved: bool 18 | reason: str 19 | 20 | class TaskPlannerAgentOutputState(TypedDict): 21 | tasks: list[str] 22 | 23 | class TaskPlannerAgentState( 24 | TaskPlannerAgentInputState, 25 | TaskPlannerAgentPrivateState, 26 | TaskPlannerAgentOutputState 27 | ): 28 | pass 29 | 30 | class DecomposedTasks(BaseModel): 31 | tasks: list[str] = Field( 32 | default_factory=list, 33 | min_length=3, 34 | max_length=5, 35 | description="分解されたタスクのリスト", 36 | ) 37 | 38 | class TaskPlannerDecomposer: 39 | def __init__(self, llm: ChatOpenAI): 40 | self.llm = llm 41 | self.current_date = datetime.now().strftime("%Y-%m-%d") 42 | 43 | def __call__(self, state: TaskPlannerAgentState) -> dict: 44 | decomposed_tasks = self.run( 45 | messages=state.get("messages", []), 46 | latest_decomposed_tasks=state.get("tasks", []), 47 | reason=state.get("reason", "") 48 | ) 49 | # 新しいタスクリストを作ったらreasonはクリアする 50 | return {"tasks": decomposed_tasks.tasks, "reason": ""} 51 | 52 | def run( 53 | self, 54 | messages: list[BaseMessage], 55 | latest_decomposed_tasks: list[str] | None, 56 | reason: str 57 | ) -> DecomposedTasks: 58 | existing_tasks = latest_decomposed_tasks if latest_decomposed_tasks else [] 59 | formatted_tasks = "\n".join([f" - {task}" for task in existing_tasks]) 60 | formatted_messages = "\n".join([f" - {message.content}" for message in messages]) 61 | 62 | prompt_text = ( 63 | f"CURRENT_DATE: {self.current_date}\n" 64 | "-----\n" 65 | ) 66 | # 改善が必要な場合のみ、その旨と理由、既存のタスクリストを追加 67 | if reason: 68 | prompt_text += ( 69 | "これまでに作成した検索タスクリストに改善が必要と判断されました。\n" 70 | f"改善が必要な理由: {reason}\n" 71 | "\n" 72 | "あなたはこの理由を参考に、検索タスクリストを再度分解・改善してください。\n" 73 | "\n" 74 | "既存の検索タスクリスト:\n" 75 | f"{existing_tasks}\n" 76 | ) 77 | else: 78 | prompt_text += ( 79 | "ユーザーの目標を達成するために必要な検索タスクを分解してください。\n" 80 | ) 81 | 82 | prompt_text += ( 83 | "\n" 84 | "要件:\n" 85 | "1. 全てのタスクはインターネット検索による情報収集のみとすること。\n" 86 | "2. 各検索タスクは以下の形式で記述すること:\n" 87 | " 「〜について検索して情報を収集する」\n" 88 | "3. 各検索タスクは具体的なキーワードや調査項目を含み、何を調べるべきか明確であること。\n" 89 | "4. 検索タスクは論理的な順序で配置すること。\n" 90 | "5. 検索タスクは日本語で記述し、必ず5個までにすること。\n\n" 91 | "ユーザーの目標:\n" 92 | "{messages}\n\n" 93 | "これらを考慮し、最適な検索タスクリストを生成してください。" 94 | ) 95 | 96 | prompt = ChatPromptTemplate.from_template(prompt_text) 97 | chain = prompt | self.llm.with_structured_output(DecomposedTasks) 98 | return chain.invoke({"messages": formatted_messages, "existing_tasks": formatted_tasks}) 99 | 100 | 101 | class TaskPlannerApprovalDecision(BaseModel): 102 | is_approved: bool = Field(description="Trueなら改善不要、Falseなら再改善") 103 | reason: str = Field(default="", description="改善が必要な場合、その理由を説明する") 104 | 105 | class TaskPlannerCheckApproval: 106 | def __init__(self, llm: ChatOpenAI): 107 | self.llm = llm 108 | 109 | def __call__(self, state: TaskPlannerAgentState) -> dict: 110 | decision = self.run(state.get("messages", []), state.get("tasks", [])) 111 | return {"is_approved": decision.is_approved, "reason": decision.reason} 112 | 113 | def run(self, messages: list[BaseMessage], tasks: list[str]) -> TaskPlannerApprovalDecision: 114 | formatted_tasks = "\n".join([f" - {task}" for task in tasks]) 115 | messages_str = "\n".join([f" - {message.content}" for message in messages]) 116 | prompt = ChatPromptTemplate.from_template( 117 | "以下はユーザーの目標と、それを達成するために提案された検索タスクリストです。\n" 118 | "検索タスクリストを評価し、以下の基準で判断してください:\n\n" 119 | "評価基準:\n" 120 | "1. 全ての検索タスクが情報収集に焦点を当てているか\n" 121 | "2. 検索内容が具体的で明確か\n" 122 | "3. ユーザーの目標達成に必要な情報を網羅しているか\n\n" 123 | "上記の基準を全て満たし、ユーザーの目標達成に十分な検索タスクが含まれている場合は" 124 | "is_approvedをTrueにしてください。\n" 125 | "改善が必要な場合はFalseにし、どの基準が満たされていないか、" 126 | "どのように改善すべきか具体的な理由をreasonフィールドに記載してください。\n\n" 127 | "### ユーザーの目標\n" 128 | "{messages}\n\n" 129 | "### タスクリスト\n" 130 | "{tasks}\n" 131 | ) 132 | chain = prompt | self.llm.with_structured_output(TaskPlannerApprovalDecision) 133 | return chain.invoke({"messages": messages_str, "tasks": formatted_tasks}) 134 | 135 | class TaskPlannerAgent: 136 | def __init__(self, llm: ChatOpenAI): 137 | self.llm = llm 138 | self.task_decomposer = TaskPlannerDecomposer(llm) 139 | self.approval_checker = TaskPlannerCheckApproval(llm) 140 | self.graph = self._create_graph() 141 | 142 | def _create_graph(self) -> CompiledStateGraph: 143 | graph = StateGraph( 144 | state_schema=TaskPlannerAgentState, 145 | input=TaskPlannerAgentInputState, 146 | output=TaskPlannerAgentOutputState, 147 | ) 148 | 149 | graph.add_node("decompose_query", self.task_decomposer) 150 | graph.add_node("check_approval", self.approval_checker) 151 | 152 | graph.add_edge(START, "decompose_query") 153 | graph.add_edge("decompose_query", "check_approval") 154 | graph.add_conditional_edges( 155 | "check_approval", 156 | lambda state: state["is_approved"], 157 | { 158 | True: END, 159 | False: "decompose_query", 160 | }, 161 | ) 162 | 163 | return graph.compile() 164 | 165 | def create_agent(llm: ChatOpenAI) -> CompiledStateGraph: 166 | return TaskPlannerAgent(llm).graph 167 | 168 | graph = create_agent(ChatOpenAI(model="gpt-4o-mini")) -------------------------------------------------------------------------------- /17/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "sd-17" 3 | version = "0.1.0" 4 | description = "Add your description here" 5 | readme = "README.md" 6 | requires-python = ">=3.11" 7 | dependencies = [ 8 | "langchain-anthropic>=0.3.0", 9 | "langchain-community>=0.3.7", 10 | "langchain-core>=0.3.19", 11 | "langchain-openai>=0.2.8", 12 | "langgraph>=0.2.50", 13 | "tavily-python>=0.5.0", 14 | ] 15 | 16 | [tool.uv] 17 | dev-dependencies = [ 18 | "langgraph-cli[inmem]>=0.1.61", 19 | ] 20 | -------------------------------------------------------------------------------- /17/sample/subgraph_sample.py: -------------------------------------------------------------------------------- 1 | from typing import TypedDict 2 | 3 | from langgraph.graph import END, START, StateGraph 4 | 5 | 6 | # サブグラフのステート定義 7 | class SubgraphState(TypedDict): 8 | messages: list[str] 9 | 10 | def subgraph_node_1(state: SubgraphState): 11 | return {"messages": state.get("messages", []) + ["subgraph_node_1"]} 12 | 13 | def subgraph_node_2(state: SubgraphState): 14 | return {"messages": state.get("messages", []) + ["subgraph_node_2"]} 15 | 16 | subgraph_builder = StateGraph(SubgraphState) 17 | subgraph_builder.add_node("sub_node_1", subgraph_node_1) 18 | subgraph_builder.add_node("sub_node_2", subgraph_node_2) 19 | subgraph_builder.add_edge(START, "sub_node_1") 20 | subgraph_builder.add_edge("sub_node_1", "sub_node_2") 21 | subgraph_builder.add_edge("sub_node_2", END) 22 | compiled_subgraph = subgraph_builder.compile() 23 | 24 | # 親グラフのステート定義 25 | class ParentState(TypedDict): 26 | messages: list[str] 27 | 28 | def parent_node_1(state: ParentState): 29 | return {"messages": state.get("messages", []) + ["parent_node_1"]} 30 | 31 | def parent_node_2(state: ParentState): 32 | return {"messages": state.get("messages", []) + ["parent_node_2"]} 33 | 34 | parent_builder = StateGraph(ParentState) 35 | parent_builder.add_node("parent_node_1", parent_node_1) 36 | parent_builder.add_node("parent_node_2", parent_node_2) 37 | 38 | # サブグラフをノードとして追加 39 | parent_builder.add_node("subgraph_node", compiled_subgraph) 40 | 41 | parent_builder.add_edge(START, "parent_node_1") 42 | parent_builder.add_edge("parent_node_1", "subgraph_node") 43 | parent_builder.add_edge("subgraph_node", "parent_node_2") 44 | parent_builder.add_edge("parent_node_2", END) 45 | parent_graph = parent_builder.compile() 46 | 47 | if __name__ == "__main__": 48 | # subgraphs=Trueを指定することで、サブグラフのノードも逐次実行される 49 | for chunk in parent_graph.stream({"messages": []}, stream_mode="values", subgraphs=True): 50 | print(chunk) 51 | -------------------------------------------------------------------------------- /18/.env.sample: -------------------------------------------------------------------------------- 1 | ANTHROPIC_API_KEY=your_anthropic_api_key_here 2 | TAVILY_API_KEY=your_tavily_api_key_here 3 | LANGSMITH_TRACING_V2=true 4 | LANGSMITH_API_KEY=your_langsmith_api_key_here 5 | LANGSMITH_PROJECT=your_langsmith_project_name_here 6 | -------------------------------------------------------------------------------- /18/.gitignore: -------------------------------------------------------------------------------- 1 | # Python 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | *.so 6 | .Python 7 | build/ 8 | develop-eggs/ 9 | dist/ 10 | downloads/ 11 | eggs/ 12 | .eggs/ 13 | lib/ 14 | lib64/ 15 | parts/ 16 | sdist/ 17 | var/ 18 | wheels/ 19 | *.egg-info/ 20 | .installed.cfg 21 | *.egg 22 | 23 | # Virtual Environment 24 | .env 25 | .venv 26 | env/ 27 | venv/ 28 | ENV/ 29 | 30 | # IDE 31 | .idea/ 32 | .vscode/ 33 | *.swp 34 | *.swo 35 | 36 | # Project specific 37 | output/ 38 | .langgraph_api/ 39 | tmp/ -------------------------------------------------------------------------------- /18/.python-version: -------------------------------------------------------------------------------- 1 | 3.12 2 | -------------------------------------------------------------------------------- /18/.repomixignore: -------------------------------------------------------------------------------- 1 | # Add patterns to ignore here, one per line 2 | # Example: 3 | # *.log 4 | # tmp/ 5 | uv.lock 6 | tmp/ 7 | .venv/ 8 | .env 9 | repomix.config.json 10 | .repomixignore 11 | .gitignore -------------------------------------------------------------------------------- /18/README.md: -------------------------------------------------------------------------------- 1 | # Software Design誌「実践LLMアプリケーション開発」第18回サンプルコード 2 | 3 | ## サンプルコードの実行方法 4 | 5 | ### 1. 日本語フォントのインストール 6 | 7 | Ubuntuの場合: 8 | ```bash 9 | sudo apt-get update 10 | sudo apt-get install -y fonts-ipafont-gothic fonts-noto-cjk 11 | ``` 12 | 13 | macOSの場合: 14 | ```bash 15 | brew install font-ipa 16 | ``` 17 | 18 | ### 2. プロジェクトのセットアップ 19 | 20 | ※ このプロジェクトは`uv`を使用しています。`uv`のインストール方法については[こちら](https://github.com/astral-sh/uv)をご確認ください。 21 | 22 | 以下のコマンドを実行し、必要なライブラリのインストールを行って下さい。 23 | 24 | ``` 25 | $ uv sync 26 | ``` 27 | 28 | 次に環境変数の設定を行います。まず`.env.sample`ファイルをコピーして`.env`ファイルを作成します。 29 | 30 | ``` 31 | $ cp .env.sample .env 32 | $ vi .env # お好きなエディタで編集してください 33 | ``` 34 | 35 | `.env`ファイルを編集し、以下のAPIキーを設定してください。 36 | 37 | - `ANTHROPIC_API_KEY`: Claude APIのキー 38 | - `TAVILY_API_KEY`: Tavily Search APIのキー 39 | - `LANGSMITH_API_KEY`: LangSmith APIのキー(オプション) 40 | - `LANGSMITH_PROJECT`: LangSmithプロジェクト名(オプション) 41 | 42 | ## 実行方法 43 | 44 | ```bash 45 | uv run python -m sd_18.agent 46 | ``` 47 | 48 | ## プロジェクト構成 49 | 50 | - `sd_18/agent.py`: メインのエージェントコード 51 | - `output/`: 生成されたファイルの保存先(gitignore対象) 52 | - `{TIMESTAMP}/`: 実行時のタイムスタンプ(YYYYMMDD_HHMMSS形式) 53 | - `charts/`: 生成されたチャートの保存先 54 | - `data/`: 生成されたデータセットの保存先 55 | 56 | ## 注意事項 57 | 58 | - チャートの生成には日本語フォントが必要です 59 | - 生成されたファイルは`output/{TIMESTAMP}`ディレクトリ以下に保存されます 60 | - チャート: `output/{TIMESTAMP}/charts/` 61 | - データセット: `output/{TIMESTAMP}/data/` 62 | - 各実行結果は実行時のタイムスタンプ付きディレクトリで管理されます 63 | - APIキーは`.env`ファイルで管理し、Gitにコミットしないようにしてください 64 | -------------------------------------------------------------------------------- /18/prompts/code_generator.prompt: -------------------------------------------------------------------------------- 1 | あなたはPythonコードを生成する専門家です。 2 | リサーチャーと協力してデータの処理と可視化を行います。 3 | 4 | ### チャート生成 5 | 1. 保存先: 'output/{timestamp}/charts/' 6 | 2. ファイル名は内容が分かる具体的な名前 7 | 3. plt.savefig()で保存してから表示 8 | 4. 日本語フォント設定: 9 | ```python 10 | import matplotlib.pyplot as plt 11 | plt.rcParams['font.family'] = 'IPAGothic' 12 | ``` 13 | 14 | ### データセット作成 15 | 1. 保存先: 'output/{timestamp}/data/' 16 | 2. ファイル名は内容が分かる具体的な名前 17 | 3. CSV形式、UTF-8エンコーディング 18 | 4. 例: 19 | ```python 20 | import pandas as pd 21 | df.to_csv('output/{timestamp}/data/dataset.csv', index=False, encoding='utf-8') 22 | ``` 23 | 24 | ### グラフ作成基準 25 | 1. タイトル・軸ラベルは日本語 26 | 2. 凡例は必要時のみ 27 | 3. グリッド線は適宜追加 28 | 4. 時系列データは折れ線グラフ -------------------------------------------------------------------------------- /18/prompts/reflection.prompt: -------------------------------------------------------------------------------- 1 | あなたはデータの品質管理の専門家です。 2 | リサーチャーとコードジェネレータの成果物を検証します。 3 | 4 | ### 出力フォーマット 5 | 必ず以下の形式で出力してください: 6 | 7 | ``` 8 | ## 1. リサーチ結果の検証 9 | ### 情報源の信頼性 10 | - [x] 公式データまたは信頼できる情報源か 11 | - [x] データの鮮度は十分か 12 | - [x] 情報源は明確に記録されているか 13 | 14 | ### データの品質 15 | - [x] 要求された詳細度を満たしているか 16 | - [x] データの欠損や異常値はないか 17 | - [x] 数値の単位は適切に記録されているか 18 | 19 | ### データ量の十分性 20 | - [x] 時系列データの場合、12ポイント以上あるか 21 | - [x] カテゴリデータの場合、5項目以上あるか 22 | - [x] 追加で必要なデータはないか 23 | 24 | ### 追加情報の必要性 25 | - [x] 必要な情報は全て揃っているか 26 | - [x] 補完が必要なデータはあるか 27 | 28 | ## 2. 生成データの検証 29 | ### CSVファイル 30 | - [x] ファイルは正しく生成されているか 31 | - [x] データ形式は適切か 32 | - [x] 元の情報と整合しているか 33 | 34 | ### チャート 35 | - [x] ファイルは正しく生成されているか 36 | - [x] グラフの種類は適切か 37 | - [x] タイトルと軸ラベルは明確か 38 | - [x] データの可視化は直感的か 39 | 40 | ## 3. 判定 41 | 判定結果: [承認 / 要改善] 42 | 次のノード: [researcher / code_generator / end] 43 | 44 | ## 4. アクション 45 | ### 改善が必要な場合 46 | 改善点: 47 | 1. (具体的な改善点を列挙) 48 | 優先度: [高/中/低] 49 | 50 | ### 承認の場合 51 | FINAL ANSWER 52 | 成果物の概要: 53 | - (生成されたファイルの説明) 54 | 特筆事項: 55 | - (データの特徴や注意点) 56 | ``` 57 | 58 | ### 検証手順 59 | 1. 必ずfile_readerツールを使用して生成ファイルの内容を確認 60 | 2. チェックリストの各項目を順番に確認([x]で完了をマーク) 61 | 3. 問題を発見したら、具体的な改善点を指示 62 | 4. 判定結果と次のノードを必ず以下の規則で記載: 63 | - リサーチ結果に問題がある場合: 64 | 判定結果: 要改善 65 | 次のノード: researcher 66 | - リサーチ結果が良好でコード生成が必要な場合: 67 | 判定結果: 承認 68 | 次のノード: code_generator 69 | - 生成結果が完了し問題ない場合: 70 | 判定結果: 承認 71 | 次のノード: end 72 | 5. 承認かつ次のノードがendの場合は必ず「FINAL ANSWER」から始まる成果物の概要を記載 73 | 74 | ### 判断基準 75 | - 情報源の信頼性: 公式データ > 信頼できる民間データ > その他 76 | - データの鮮度: 目的に応じた適切な期間のデータか 77 | - データの粒度: 分析に必要な詳細度を満たしているか 78 | - データ量: 時系列は12ポイント以上、カテゴリは5項目以上 79 | - 可視化の品質: 直感的で誤解を招かない表現か -------------------------------------------------------------------------------- /18/prompts/researcher.prompt: -------------------------------------------------------------------------------- 1 | あなたはデータリサーチの専門家です。 2 | コードジェネレータと協力して作業を行います。 3 | 4 | ### あなたの役割 5 | 1. 要求された情報を検索して収集 6 | 2. 収集したデータはCSV形式でまとめるようにコードジェネレータに依頼 7 | 3. データの形式や期間が不明な場合の基準: 8 | - 数値データは可能な限り詳細な粒度で収集 9 | - 期間未指定の場合は直近1年間 10 | - 時系列データは日次または月次を優先 11 | 12 | ### データ収集の基準 13 | 1. 最低データ量 14 | - 時系列データの場合:12ポイント以上 15 | - カテゴリデータの場合:5項目以上 16 | 2. データが少ない場合の対応 17 | - 検索キーワードを変えて再検索 18 | - 関連する指標やデータも収集 19 | - 複数の情報源からデータを収集 20 | 21 | ### 注意事項 22 | - 信頼できる情報源を選択 23 | - データの出典を記録 24 | - 数値データの単位を明記 25 | - データが少ない場合は必ず追加の検索を実施 -------------------------------------------------------------------------------- /18/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "sd-18" 3 | version = "0.1.0" 4 | description = "sample project for sd-18" 5 | readme = "README.md" 6 | requires-python = ">=3.12" 7 | dependencies = [ 8 | "japanize-matplotlib>=1.1.3", 9 | "langchain-anthropic>=0.3.1", 10 | "langchain-community>=0.3.14", 11 | "langchain-experimental>=0.3.4", 12 | "langgraph>=0.2.62", 13 | "matplotlib>=3.10.0", 14 | "pandas>=2.2.3", 15 | "python-dotenv>=1.0.1", 16 | ] 17 | -------------------------------------------------------------------------------- /18/repomix.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "output": { 3 | "filePath": "tmp/repomix-output.md", 4 | "style": "markdown", 5 | "fileSummary": true, 6 | "directoryStructure": true, 7 | "removeComments": false, 8 | "removeEmptyLines": false, 9 | "topFilesLength": 5, 10 | "showLineNumbers": false, 11 | "copyToClipboard": false 12 | }, 13 | "include": [], 14 | "ignore": { 15 | "useGitignore": true, 16 | "useDefaultPatterns": true, 17 | "customPatterns": [] 18 | }, 19 | "security": { 20 | "enableSecurityCheck": true 21 | }, 22 | "tokenCount": { 23 | "encoding": "o200k_base" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /18/sd_18/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahm/softwaredesign-llm-application/a2ab09fde6c62bd538d4170ecc319000949ebfa0/18/sd_18/__init__.py -------------------------------------------------------------------------------- /19/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | !frontend/agent_tool_for_user/lib/ 19 | node_modules/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | share/python-wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | *.py,cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | cover/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | .pybuilder/ 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 88 | # For a library or package, you might want to ignore these files since the code is 89 | # intended to run in multiple environments; otherwise, check them in: 90 | # .python-version 91 | 92 | # pipenv 93 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 94 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 95 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 96 | # install all needed dependencies. 97 | #Pipfile.lock 98 | 99 | # poetry 100 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 101 | # This is especially recommended for binary packages to ensure reproducibility, and is more 102 | # commonly ignored for libraries. 103 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 104 | #poetry.lock 105 | 106 | # pdm 107 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 108 | #pdm.lock 109 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 110 | # in version control. 111 | # https://pdm.fming.dev/#use-with-ide 112 | .pdm.toml 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # Spyder project settings 134 | .spyderproject 135 | .spyproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # mkdocs documentation 141 | /site 142 | 143 | # mypy 144 | .mypy_cache/ 145 | .dmypy.json 146 | dmypy.json 147 | 148 | # ruff 149 | .ruff_cache/ 150 | 151 | # Pyre type checker 152 | .pyre/ 153 | 154 | # pytype static type analyzer 155 | .pytype/ 156 | 157 | # Cython debug symbols 158 | cython_debug/ 159 | 160 | # PyCharm 161 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 162 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 163 | # and can be added to the global gitignore or merged into this file. For a more nuclear 164 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 165 | #.idea/ 166 | 167 | # tmp 168 | /tmp/ 169 | 170 | # React.js Outputs 171 | /spa/ 172 | 173 | # amplify 174 | node_modules 175 | .amplify 176 | amplify_outputs* 177 | amplifyconfiguration* 178 | cdk.out/ 179 | 180 | # Next.js 181 | .next 182 | 183 | .cursorrules 184 | spec.md -------------------------------------------------------------------------------- /19/.python-version: -------------------------------------------------------------------------------- 1 | 3.12 2 | -------------------------------------------------------------------------------- /19/README.md: -------------------------------------------------------------------------------- 1 | # Software Design誌「実践LLMアプリケーション開発」第19回サンプルコード 2 | 3 | ## サンプルコードの実行方法 4 | 5 | ### プロジェクトのセットアップ 6 | 7 | ※ このプロジェクトは`uv`を使用しています。`uv`のインストール方法については[こちら](https://github.com/astral-sh/uv)をご確認ください。 8 | 9 | 以下のコマンドを実行し、必要なライブラリのインストールを行って下さい。 10 | 11 | ``` 12 | $ uv sync 13 | ``` 14 | 15 | ## 実行方法 16 | 17 | 以下のコマンドを実行することで、MCPサーバの動作確認を行うことができます。 18 | 19 | ```bash 20 | uv run python -m sd_19.client 21 | ``` 22 | 23 | ## MCPサーバの組み込み方 24 | 25 | 例えばCursorエディタに本サンプルコードの`sd_19/server.py`をMCPサーバとして組み込みたい場合、以下のように設定します。 26 | 27 | | 項目 | 設定値 | 28 | |------|--------| 29 | | Name | sd-19 | 30 | | Type | Command | 31 | | Command | `uv --directory [プロジェクトのディレクトリパス]19 run sd_19/server.py` | 32 | 33 | 実際にCursorに設定した場合は、以下のように表示されます。(※ Cursor Settings > Features > MCP Servers) 34 | 35 | ![Cursorでの設定例](cursor.png) -------------------------------------------------------------------------------- /19/cursor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahm/softwaredesign-llm-application/a2ab09fde6c62bd538d4170ecc319000949ebfa0/19/cursor.png -------------------------------------------------------------------------------- /19/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "sd-19" 3 | version = "0.1.0" 4 | description = "Add your description here" 5 | readme = "README.md" 6 | requires-python = ">=3.12" 7 | dependencies = [ 8 | "mcp[cli]>=1.2.1", 9 | ] 10 | -------------------------------------------------------------------------------- /19/sd_19/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahm/softwaredesign-llm-application/a2ab09fde6c62bd538d4170ecc319000949ebfa0/19/sd_19/__init__.py -------------------------------------------------------------------------------- /19/sd_19/client.py: -------------------------------------------------------------------------------- 1 | from mcp import ClientSession, StdioServerParameters 2 | from mcp.client.stdio import stdio_client 3 | from mcp.server.fastmcp.utilities.logging import configure_logging, get_logger 4 | 5 | logger = get_logger(__name__) 6 | configure_logging() 7 | 8 | server_params = StdioServerParameters( 9 | command="python", 10 | args=["sd_19/server.py"], 11 | env=None, 12 | ) 13 | 14 | 15 | async def run(): 16 | async with stdio_client(server_params) as (read, write): 17 | async with ClientSession(read, write) as session: 18 | # 接続を初期化 19 | await session.initialize() 20 | 21 | # 利用可能なプロンプト一覧を取得 22 | prompts = await session.list_prompts() 23 | logger.info("=== prompts ===") 24 | logger.info(prompts) 25 | logger.info("\n") 26 | 27 | # プロンプトを取得 28 | prompt = await session.get_prompt( 29 | "greet_user", arguments={"user_name": "sd-19"} 30 | ) 31 | logger.info("=== prompt ===") 32 | logger.info(prompt) 33 | logger.info("\n") 34 | 35 | # 利用可能なリソース一覧を取得 36 | resources = await session.list_resources() 37 | logger.info("=== resources ===") 38 | logger.info(resources) 39 | logger.info("\n") 40 | 41 | # リソースを読み込む 42 | resource = await session.read_resource("file://readme.md") 43 | logger.info("=== resource ===") 44 | logger.info(resource) 45 | logger.info("\n") 46 | 47 | # 利用可能なツール一覧を取得 48 | tools = await session.list_tools() 49 | logger.info("=== tools ===") 50 | logger.info(tools) 51 | logger.info("\n") 52 | 53 | # ツールを呼び出す 54 | result = await session.call_tool("hello", arguments={"name": "sd-19"}) 55 | logger.info("=== tool result ===") 56 | logger.info(result) 57 | logger.info("\n") 58 | 59 | 60 | if __name__ == "__main__": 61 | import asyncio 62 | 63 | asyncio.run(run()) 64 | -------------------------------------------------------------------------------- /19/sd_19/server.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import List 3 | 4 | from mcp.server.fastmcp import FastMCP 5 | from mcp.server.fastmcp.prompts.base import AssistantMessage, Message, UserMessage 6 | 7 | # 定数定義 8 | DEFAULT_HOST = "0.0.0.0" 9 | DEFAULT_PORT = 8080 10 | 11 | mcp = FastMCP("sd-19-mcp") 12 | 13 | 14 | @mcp.tool() 15 | def hello(name: str) -> str: 16 | """ 17 | 与えられた名前に対して挨拶を返します。 18 | """ 19 | return f"Hello, {name}!" 20 | 21 | 22 | @mcp.prompt() 23 | def greet_user(user_name: str) -> List[Message]: 24 | """ 25 | ユーザーに挨拶するための定型プロンプトを返します。 26 | 返値はMCPで規定されたメッセージのリストです。 27 | """ 28 | return [ 29 | UserMessage(content=f"{user_name}さん、こんにちは。"), 30 | AssistantMessage(content="どのようにお手伝いできますか?"), 31 | ] 32 | 33 | 34 | @mcp.resource("file://readme.md") 35 | def readme() -> str: 36 | """ 37 | README.md を読み込んで返します。 38 | """ 39 | current_dir = os.path.dirname(os.path.abspath(__file__)) 40 | readme_path = os.path.normpath(os.path.join(current_dir, "..", "README.md")) 41 | 42 | if not os.path.exists(readme_path): 43 | raise FileNotFoundError(f"README.md not found at: {readme_path}") 44 | 45 | with open(readme_path, "r") as f: 46 | content = f.read() 47 | return content 48 | 49 | 50 | def start_server( 51 | transport: str, host: str = DEFAULT_HOST, port: int = DEFAULT_PORT 52 | ) -> None: 53 | """MCPサーバーを起動する 54 | 55 | Args: 56 | transport (str): 使用するトランスポートモード ('stdio' または 'sse') 57 | host (str, optional): SSEモード時のホスト名. デフォルトは DEFAULT_HOST 58 | port (int, optional): SSEモード時のポート番号. デフォルトは DEFAULT_PORT 59 | """ 60 | try: 61 | if transport == "stdio": 62 | mcp.run(transport="stdio") 63 | elif transport == "sse": 64 | mcp.run(transport="sse", host=host, port=port) 65 | else: 66 | raise ValueError(f"不正なトランスポートモード: {transport}") 67 | except Exception as e: 68 | print(f"サーバー起動エラー: {e}") 69 | raise 70 | 71 | 72 | if __name__ == "__main__": 73 | import argparse 74 | 75 | parser = argparse.ArgumentParser(description="MCPサーバーの起動モードを指定") 76 | parser.add_argument( 77 | "--transport", 78 | type=str, 79 | choices=["stdio", "sse"], 80 | default="stdio", 81 | help="使用するトランスポートモード (stdio または sse)", 82 | ) 83 | parser.add_argument( 84 | "--host", 85 | type=str, 86 | default=DEFAULT_HOST, 87 | help="ホスト名", 88 | ) 89 | parser.add_argument( 90 | "--port", 91 | type=int, 92 | default=DEFAULT_PORT, 93 | help="ポート番号", 94 | ) 95 | args = parser.parse_args() 96 | start_server( 97 | transport=args.transport, 98 | host=args.host, 99 | port=args.port, 100 | ) 101 | -------------------------------------------------------------------------------- /20/.env.sample: -------------------------------------------------------------------------------- 1 | # Anthropic 2 | ANTHROPIC_API_KEY=your_anthropic_api_key 3 | 4 | # Tavily 5 | TAVILY_API_KEY=your_tavily_api_key 6 | 7 | # Langsmith 8 | LANGSMITH_TRACING_V2="true" 9 | LANGSMITH_API_KEY=your_langsmith_api_key 10 | LANGSMITH_PROJECT="sd-20" 11 | 12 | # Database 13 | DB_PATH=data.db -------------------------------------------------------------------------------- /20/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | !frontend/agent_tool_for_user/lib/ 19 | node_modules/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | share/python-wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | *.py,cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | cover/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | .pybuilder/ 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 88 | # For a library or package, you might want to ignore these files since the code is 89 | # intended to run in multiple environments; otherwise, check them in: 90 | # .python-version 91 | 92 | # pipenv 93 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 94 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 95 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 96 | # install all needed dependencies. 97 | #Pipfile.lock 98 | 99 | # poetry 100 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 101 | # This is especially recommended for binary packages to ensure reproducibility, and is more 102 | # commonly ignored for libraries. 103 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 104 | #poetry.lock 105 | 106 | # pdm 107 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 108 | #pdm.lock 109 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 110 | # in version control. 111 | # https://pdm.fming.dev/#use-with-ide 112 | .pdm.toml 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # Spyder project settings 134 | .spyderproject 135 | .spyproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # mkdocs documentation 141 | /site 142 | 143 | # mypy 144 | .mypy_cache/ 145 | .dmypy.json 146 | dmypy.json 147 | 148 | # ruff 149 | .ruff_cache/ 150 | 151 | # Pyre type checker 152 | .pyre/ 153 | 154 | # pytype static type analyzer 155 | .pytype/ 156 | 157 | # Cython debug symbols 158 | cython_debug/ 159 | 160 | # PyCharm 161 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 162 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 163 | # and can be added to the global gitignore or merged into this file. For a more nuclear 164 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 165 | #.idea/ 166 | 167 | # tmp 168 | /tmp/ 169 | 170 | # Next.js 171 | .next 172 | 173 | # Database 174 | data.db 175 | 176 | .langgraph_api/ -------------------------------------------------------------------------------- /20/.python-version: -------------------------------------------------------------------------------- 1 | 3.12 2 | -------------------------------------------------------------------------------- /20/.repomixignore: -------------------------------------------------------------------------------- 1 | # Add patterns to ignore here, one per line 2 | # Example: 3 | # *.log 4 | tmp/ 5 | .langgraph_api/ 6 | .mypy_cache/ 7 | .venv/ 8 | *.db 9 | uv.lock 10 | -------------------------------------------------------------------------------- /20/README.md: -------------------------------------------------------------------------------- 1 | # Software Design誌「実践LLMアプリケーション開発」第20回サンプルコード 2 | 3 | ## サンプルコードの実行方法 4 | 5 | ### プロジェクトのセットアップ 6 | 7 | ※ このプロジェクトは`uv`を使用しています。`uv`のインストール方法については[こちら](https://github.com/astral-sh/uv)をご確認ください。 8 | 9 | 以下のコマンドを実行し、必要なライブラリのインストールを行って下さい。 10 | 11 | ``` 12 | $ uv sync 13 | ``` 14 | 15 | 次に環境変数の設定を行います。まず`.env.sample`ファイルをコピーして`.env`ファイルを作成します。 16 | 17 | ``` 18 | $ cp .env.sample .env 19 | $ vi .env # お好きなエディタで編集してください 20 | ``` 21 | 22 | 続けて.envファイル内の以下のキーを設定して下さい。`TAVILY_API_KEY`ならびに`LANGSMITH_API_KEY`の取得方法については次節で解説します。 23 | 24 | ``` 25 | ANTHROPIC_API_KEY=[発行されたAPIキー] 26 | TAVILY_API_KEY=[発行されたAPIキー] 27 | LANGSMITH_API_KEY=[発行されたAPIキー] 28 | ``` 29 | 30 | ## 実行方法 31 | 32 | 以下のコマンドを実行することで、エージェントを実行できます。 33 | MCPサーバは自動的に起動されるため、事前に別途起動する必要はありません。 34 | 35 | ```bash 36 | # エージェントの実行(コマンドライン引数でエージェントへの依頼を設定) 37 | uv run -m src.sd_20 "LangChainの最新情報を教えてください" 38 | ``` 39 | 40 | また、LangGraph Studioを利用して動作確認をすることもできます。 41 | LangGraph Studioを利用する場合は、次のコマンドでサーバを立ち上げてください。 42 | 43 | ```bash 44 | uv run langgraph dev 45 | ``` 46 | 47 | ## mcp_config.jsonの設定 48 | 49 | このサンプルコードでは、MCPサーバの設定を`mcp_config.json`ファイルで管理しています。デフォルトでは以下のような設定になっています: 50 | 51 | ```json 52 | { 53 | "mcpServers": { 54 | "knowledge-db": { 55 | "command": "uv", 56 | "args": ["run", "-m", "src.mcp_servers.server"] 57 | } 58 | } 59 | } 60 | ``` 61 | 62 | ## サンプルコードの内容 63 | 64 | 本サンプルコードでは、MCPサーバとLangGraphエージェント(`create_react_agent`)との連携を実装しています。 65 | 66 | 主要なコンポーネント: 67 | - `src/sd_20/mcp_manager.py`: MCPサーバからツールをロードし、LangGraphエージェントで使えるようにする 68 | - `src/sd_20/agent.py`: `create_react_agent`を使用したエージェントの定義 69 | - `src/mcp_servers/database.py`: SQLiteの操作を行うモジュール 70 | - `src/mcp_servers/server.py`: MCPサーバの実装 71 | 72 | ## Tavily APIキーの取得方法 73 | 74 | ### Tavilyについて 75 | 76 | Tavilyは、LLMアプリケーションにおけるRAG(Retrieval-Augmented Generation)に特化した検索エンジンです。 77 | 78 | 通常の検索APIはユーザーのクエリに基づいて検索結果を取得しますが、検索の目的に対して無関係な結果が返ってくることがあります。また、単純なURLやスニペットが返されるため、開発者が関連するコンテンツをスクレイピングしたり、不要な情報をフィルタリングしたりする必要があります。 79 | 80 | 一方でTavilyは、一回のAPI呼び出しで20以上のサイトを集約し、独自のAIを利用しタスクやクエリ、目的に最も関連する情報源とコンテンツを評価、フィルタリング、ランク付けすることが可能な仕組みになっています。 81 | 82 | 今回のサンプルコードを実行するためにはTavilyのAPIキーが必要なので、以下の手段でAPIキーを取得してください。 83 | 84 | ### Tavily APIキーの取得 85 | 86 | 1. まず https://tavily.com/ にアクセスし、画面右上の「Try it out」をクリックします。 87 | 2. するとサインイン画面に遷移するので、そのままサインインを進めて下さい。GoogleアカウントかGitHubアカウントを選択することができます。 88 | 3. サインインが完了するとAPIキーが取得できる画面に遷移します。画面中央部の「API Key」欄より、APIキーをコピーしてください。 89 | 4. APIキーを`.env`内の`TAVILY_API_KEY`に設定してください。 90 | 91 | ## LangSmith APIキーの取得方法 92 | 93 | LangSmithはLLM(大規模言語モデル)実行のログ分析ツールです。LLMアプリケーションの実行を詳細に追跡し、デバッグや評価を行うための機能を提供します。詳細については https://docs.smith.langchain.com/ をご確認ください。 94 | 95 | LangSmithによるトレースを有効にするためには、LangSmithにユーザー登録した上で、APIキーを発行する必要があります。以下の手順を参考にAPIキーを取得してください。 96 | 97 | ### LangSmith APIキーの取得 98 | 99 | 1. [サインアップページ](https://smith.langchain.com/)より、LangSmithのユーザー登録を行います。 100 | 2. [設定ページ](https://smith.langchain.com/settings)を開きます。 101 | 3. API keysタブを開き、「Create API Key」をクリックします。するとAPIキーが発行されますので、APIキーをコピーしてください。 102 | 4. APIキーを`.env`内の`LANGCHAIN_API_KEY`に設定してください。 103 | -------------------------------------------------------------------------------- /20/langgraph.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": ["."], 3 | "graphs": { 4 | "agent": "./src/sd_20/agent.py:graph" 5 | }, 6 | "env": ".env" 7 | } 8 | -------------------------------------------------------------------------------- /20/mcp_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "knowledge-db": { 4 | "command": "uv", 5 | "args": ["run", "-m", "src.mcp_servers.server"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /20/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "sd-20" 3 | version = "0.1.0" 4 | description = "Add your description here" 5 | readme = "README.md" 6 | requires-python = ">=3.12" 7 | dependencies = [ 8 | "langchain-anthropic>=0.3.10", 9 | "langchain-core>=0.3.48", 10 | "langgraph-checkpoint>=2.0.21", 11 | "langgraph>=0.3.16", 12 | "mcp[cli]>=1.4.1", 13 | "python-dotenv>=1.0.1", 14 | "tavily-python>=0.5.1", 15 | ] 16 | 17 | [dependency-groups] 18 | dev = [ 19 | "langgraph-cli[inmem]>=0.1.77", 20 | ] 21 | -------------------------------------------------------------------------------- /20/repomix.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "output": { 3 | "filePath": "tmp/repomix-output.txt", 4 | "style": "plain", 5 | "parsableStyle": false, 6 | "fileSummary": true, 7 | "directoryStructure": true, 8 | "removeComments": false, 9 | "removeEmptyLines": false, 10 | "compress": false, 11 | "topFilesLength": 5, 12 | "showLineNumbers": false, 13 | "copyToClipboard": false, 14 | "git": { 15 | "sortByChanges": true, 16 | "sortByChangesMaxCommits": 100 17 | } 18 | }, 19 | "include": [], 20 | "ignore": { 21 | "useGitignore": true, 22 | "useDefaultPatterns": true, 23 | "customPatterns": [] 24 | }, 25 | "security": { 26 | "enableSecurityCheck": true 27 | }, 28 | "tokenCount": { 29 | "encoding": "o200k_base" 30 | } 31 | } -------------------------------------------------------------------------------- /20/src/mcp_servers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahm/softwaredesign-llm-application/a2ab09fde6c62bd538d4170ecc319000949ebfa0/20/src/mcp_servers/__init__.py -------------------------------------------------------------------------------- /20/src/mcp_servers/server.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import sys 4 | 5 | from mcp.server.fastmcp import FastMCP 6 | from tavily import TavilyClient # type: ignore 7 | 8 | import src.mcp_servers.database as db 9 | 10 | print("MCPサーバーの初期化を開始します...") 11 | 12 | # MCPサーバーの初期化 13 | mcp = FastMCP("knowledge-db-mcp-server") 14 | 15 | print("データベース操作ツールを登録します...") 16 | 17 | # --- ツール定義 --- 18 | 19 | # 1. Tavily APIを使ったWeb検索ツール 20 | TAVILY_API_KEY = os.getenv("TAVILY_API_KEY") 21 | tavily_client = TavilyClient(api_key=TAVILY_API_KEY) 22 | 23 | 24 | @mcp.tool() 25 | def search_web(query: str, max_results: int = 5) -> str: 26 | """ 27 | Tavily APIを使用してWeb検索を行い、上位の結果を返します。 28 | 29 | 引数: 30 | query: 検索クエリ 31 | max_results: 返す検索結果の最大数 (デフォルト: 5) 32 | 33 | 返値: 34 | 検索結果のテキスト(各結果のタイトル・URL・スニペット) 35 | """ 36 | response = tavily_client.search(query, max_results=max_results) 37 | answer = response.get("answer") 38 | result_text = "" 39 | if answer: 40 | result_text += f"回答: {answer}\n\n" 41 | 42 | results = response.get("results", []) 43 | if not results: 44 | return result_text + "検索結果が見つかりませんでした。" 45 | 46 | for i, res in enumerate(results, start=1): 47 | title = res.get("title", "(タイトルなし)") 48 | url = res.get("url", "") 49 | snippet = res.get("content") or res.get("snippet") or "" 50 | result_text += f"{i}. {title}\n URL: {url}\n" 51 | if snippet: 52 | result_text += f" 概要: {snippet}\n\n" 53 | 54 | return result_text.strip() 55 | 56 | 57 | @mcp.tool() 58 | def extract_urls( 59 | urls: list, include_images: bool = False, max_content_length: int = 5_000 60 | ) -> str: 61 | """ 62 | 指定されたURLリストの内容を抽出します。 63 | 64 | 引数: 65 | urls: 抽出するURLのリスト(最大20件まで) 66 | include_images: 画像情報も含めるかどうか (デフォルト: False) 67 | max_content_length: コンテンツの最大文字数 (デフォルト: 5,000) 68 | 返値: 69 | 抽出されたコンテンツの情報(タイトル、URL、本文など) 70 | """ 71 | if not isinstance(urls, list): 72 | urls = [urls] # 単一のURLが文字列で渡された場合、リストに変換 73 | 74 | if len(urls) > 20: 75 | return "エラー: 一度に処理できるURLは最大20件までです。" 76 | 77 | try: 78 | response = tavily_client.extract(urls=urls, include_images=include_images) 79 | 80 | result_text = "" 81 | for result in response.get("results", []): 82 | url = result.get("url", "") 83 | title = result.get("title", "(タイトルなし)") 84 | raw_content = result.get("raw_content", "") 85 | images = result.get("images", []) 86 | 87 | result_text += f"URL: {url}\n" 88 | result_text += f"タイトル: {title}\n" 89 | 90 | # コンテンツは長くなる可能性があるため、指定された最大文字数のみ表示 91 | content_preview = ( 92 | raw_content[:max_content_length] + "..." 93 | if len(raw_content) > max_content_length 94 | else raw_content 95 | ) 96 | result_text += f"コンテンツ: {content_preview}\n" 97 | 98 | if include_images and images: 99 | result_text += f"画像数: {len(images)}\n" 100 | # 最初の3つの画像URLのみ表示 101 | for i, img in enumerate(images[:3], start=1): 102 | result_text += f" 画像{i}: {img}\n" 103 | if len(images) > 3: 104 | result_text += f" 他 {len(images) - 3} 枚の画像\n" 105 | 106 | result_text += "\n---\n\n" 107 | 108 | return result_text.strip() 109 | except Exception as e: 110 | return f"URLの内容抽出中にエラーが発生しました: {str(e)}" 111 | 112 | 113 | @mcp.tool() 114 | def save_search_result( 115 | query: str, 116 | url: str, 117 | title: str, 118 | content: str = "", 119 | content_type: str = "", 120 | summary: str = "", 121 | tags: str = "", 122 | reliability_score: float = 0.5, 123 | ) -> str: 124 | """ 125 | 検索結果をデータベースに保存します。 126 | 127 | 引数: 128 | query: 検索クエリ 129 | url: 情報ソースのURL 130 | title: コンテンツのタイトル 131 | content: 抽出したコンテンツ(本文) 132 | content_type: 情報タイプ(例: "ニュース", "技術文書") 133 | summary: 要約文(エージェントが生成) 134 | tags: カンマ区切りのタグ 135 | reliability_score: 信頼性スコア (0.0-1.0) 136 | 137 | 返値: 138 | 保存処理の結果メッセージ 139 | """ 140 | result = db.save_search_result( 141 | query, url, title, content, content_type, summary, tags, reliability_score 142 | ) 143 | return result["message"] 144 | 145 | 146 | @mcp.tool() 147 | def get_recent_results(days: int = 7, limit: int = 10, content_type: str = "") -> str: 148 | """ 149 | 指定された日数以内の最近の検索結果を取得します。 150 | 151 | 引数: 152 | days: 何日前までの検索結果を取得するか (デフォルト: 7) 153 | limit: 返す結果の最大数 (デフォルト: 10) 154 | content_type: 特定のコンテンツタイプでフィルタリング (オプション) 155 | 156 | 返値: 157 | 最近の検索結果とそのサマリー(JSON形式) 158 | """ 159 | result = db.get_recent_results(days, limit, content_type) 160 | if not result["success"]: 161 | return result["message"] 162 | 163 | return json.dumps(result["results"], ensure_ascii=False, indent=2) 164 | 165 | 166 | @mcp.tool() 167 | def get_content_by_id(result_id: int) -> str: 168 | """ 169 | 特定IDの検索結果の詳細コンテンツを取得します。 170 | 171 | 引数: 172 | result_id: 検索結果のID 173 | 174 | 返値: 175 | 検索結果の詳細(タイトル、URL、コンテンツなど) 176 | """ 177 | result = db.get_content_by_id(result_id) 178 | if not result["success"]: 179 | return result["message"] 180 | 181 | return json.dumps(result["result"], ensure_ascii=False, indent=2) 182 | 183 | 184 | @mcp.tool() 185 | def get_content_types() -> str: 186 | """ 187 | データベースに保存されている全てのコンテンツタイプの一覧と各タイプの件数を返します。 188 | 189 | 返値: 190 | コンテンツタイプとその件数の一覧(JSON形式) 191 | """ 192 | result = db.get_content_types() 193 | if not result["success"]: 194 | return result["message"] 195 | 196 | return json.dumps(result["results"], ensure_ascii=False, indent=2) 197 | 198 | 199 | @mcp.tool() 200 | def get_schema() -> str: 201 | """ 202 | SQLiteデータベースのスキーマ情報(テーブル名と各カラム)を返します。 203 | この情報は、LLMが適切なクエリを作成するためのヒントとして利用されます。 204 | """ 205 | result = db.get_schema() 206 | if not result["success"]: 207 | return result["message"] 208 | 209 | return result["schema"] 210 | 211 | 212 | @mcp.tool() 213 | def select_query(query: str) -> str: 214 | """ 215 | SQLiteデータベースに対してSELECTクエリを実行し、結果を返します。 216 | 例: "SELECT * FROM search_results WHERE content_type='ニュース' LIMIT 10;" 217 | ※SELECT文のみ許可されています。 218 | """ 219 | result = db.execute_select_query(query) 220 | if not result["success"]: 221 | return result["message"] 222 | 223 | return json.dumps(result["results"], ensure_ascii=False, indent=2) 224 | 225 | 226 | if __name__ == "__main__": 227 | from dotenv import load_dotenv 228 | 229 | load_dotenv() 230 | 231 | print("MCPサーバーを起動します...") 232 | try: 233 | mcp.run(transport="stdio") 234 | except Exception as e: 235 | print(f"サーバー実行中にエラーが発生しました: {e}", file=sys.stderr) 236 | sys.exit(1) 237 | -------------------------------------------------------------------------------- /20/src/sd_20/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahm/softwaredesign-llm-application/a2ab09fde6c62bd538d4170ecc319000949ebfa0/20/src/sd_20/__init__.py -------------------------------------------------------------------------------- /20/src/sd_20/__main__.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import uuid 3 | 4 | from src.sd_20.agent import graph 5 | 6 | if __name__ == "__main__": 7 | # コマンドライン引数の処理 8 | parser = argparse.ArgumentParser(description="MCPエージェントと会話する") 9 | parser.add_argument("query", help="エージェントへの質問") 10 | args = parser.parse_args() 11 | 12 | # エージェントの実行 13 | config = {'configurable': {'thread_id': str(uuid.uuid4())}} 14 | user_input = {"messages": [("human", args.query)]} 15 | for state in graph.stream(user_input, stream_mode="values", config=config): 16 | msg = state["messages"][-1] 17 | msg.pretty_print() 18 | -------------------------------------------------------------------------------- /20/src/sd_20/agent.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from datetime import datetime 3 | 4 | from dotenv import load_dotenv 5 | from langchain_anthropic import ChatAnthropic 6 | from langgraph.checkpoint.memory import MemorySaver 7 | from langgraph.prebuilt import create_react_agent 8 | from src.sd_20.mcp_manager import load_all_mcp_tools 9 | from src.sd_20.state import CustomAgentState 10 | 11 | # 環境変数の読み込み 12 | load_dotenv() 13 | 14 | 15 | def create_agent(): 16 | # ツール(MCPツール)の読み込み 17 | tools = asyncio.run(load_all_mcp_tools()) 18 | 19 | # ツールの説明の作成 20 | tool_descriptions = "\n\n".join( 21 | [f"### {tool.name}\n{tool.description}" for tool in tools] 22 | ) 23 | 24 | # 本日の日付の取得 25 | current_date = datetime.now().strftime("%Y年%m月%d日") 26 | 27 | # プロンプトの読み込み 28 | with open("src/sd_20/prompts/system.txt", "r") as f: 29 | prompt = f.read() 30 | 31 | # プロンプトの作成 32 | prompt = prompt.format( 33 | tool_descriptions=tool_descriptions, 34 | current_date=current_date, 35 | ) 36 | 37 | # モデルの設定 38 | model = ChatAnthropic( 39 | model_name="claude-3-7-sonnet-20250219", 40 | timeout=None, 41 | stop=None, 42 | max_tokens=4_096, 43 | ) 44 | 45 | # エージェントの作成 46 | graph = create_react_agent( 47 | model, 48 | tools=tools, 49 | prompt=prompt, 50 | state_schema=CustomAgentState, 51 | checkpointer=MemorySaver(), 52 | ) 53 | 54 | return graph 55 | 56 | 57 | graph = create_agent() 58 | -------------------------------------------------------------------------------- /20/src/sd_20/mcp_manager.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | import os 4 | import sys 5 | from typing import Any, Dict, List, Optional, Tuple 6 | 7 | from langchain_core.tools.structured import StructuredTool 8 | from mcp.client.session import ClientSession 9 | from mcp.client.stdio import StdioServerParameters, stdio_client 10 | from mcp.types import Tool as MCPTool 11 | 12 | 13 | def load_mcp_config(config_path="mcp_config.json") -> Dict[str, Any]: 14 | """JSON定義の読み込み""" 15 | print(f"設定ファイル '{config_path}' を読み込みます...") 16 | with open(config_path, "r") as f: 17 | config = json.load(f) 18 | return config 19 | 20 | 21 | def get_available_servers(config: Dict[str, Any]) -> List[str]: 22 | """設定ファイルから利用可能なサーバー名のリストを取得します""" 23 | return list(config.get("mcpServers", {}).keys()) 24 | 25 | 26 | def create_server_params( 27 | config: Dict[str, Any], server_name: Optional[str] = None 28 | ) -> StdioServerParameters: 29 | """指定されたサーバー名の設定から StdioServerParameters を作成します""" 30 | available_servers = get_available_servers(config) 31 | 32 | if not available_servers: 33 | raise ValueError("設定ファイルにMCPサーバーが定義されていません") 34 | 35 | # サーバー名が指定されていない場合は最初のサーバーを使用 36 | if server_name is None: 37 | server_name = available_servers[0] 38 | elif server_name not in available_servers: 39 | raise ValueError(f"指定されたサーバー '{server_name}' は設定に存在しません") 40 | 41 | # 指定されたサーバーの設定を取得 42 | server_conf = config["mcpServers"][server_name] 43 | command = server_conf["command"] 44 | args = server_conf["args"] 45 | 46 | print(f"サーバー '{server_name}' の設定: command='{command}', args={args}") 47 | 48 | return StdioServerParameters(command=command, args=args, env=dict(os.environ)) 49 | 50 | 51 | def create_all_server_params( 52 | config: Dict[str, Any], 53 | ) -> Dict[str, StdioServerParameters]: 54 | """設定ファイルに定義されている全てのMCPサーバーのパラメータを作成""" 55 | servers = {} 56 | for server_name in get_available_servers(config): 57 | try: 58 | servers[server_name] = create_server_params(config, server_name) 59 | except Exception as e: 60 | print(f"警告: サーバー '{server_name}' の設定読み込みに失敗しました: {e}") 61 | return servers 62 | 63 | 64 | def extract_tool_list(response: Any) -> List[Any]: 65 | """MCPサーバーのレスポンスからツールリストを抽出します""" 66 | if hasattr(response, "tools"): 67 | tool_list = response.tools 68 | print(f"ツールリストを取得: {len(tool_list)}個") 69 | return tool_list 70 | else: 71 | print("警告: ツールリストを特定できませんでした") 72 | return [] 73 | 74 | 75 | def extract_tool_info(tool_item: Any) -> Tuple[str, str]: 76 | """ツールアイテムから名前と説明を抽出します""" 77 | tool_name = getattr(tool_item, "name", "") 78 | tool_desc = getattr(tool_item, "description", "") 79 | 80 | if not tool_name: 81 | print(f"警告: ツール名が見つかりません: {tool_item}") 82 | 83 | return tool_name, tool_desc 84 | 85 | 86 | async def create_langchain_tool( 87 | tool_name: str, 88 | tool_desc: str, 89 | prefix: str, 90 | server_name: Optional[str], 91 | server_params: StdioServerParameters, 92 | tool_item: MCPTool, 93 | ) -> StructuredTool: 94 | """ 95 | MCPツールをLangChainのStructuredToolとして生成 96 | """ 97 | # サーバー名をプレフィックスとしてツール名に追加(重複防止) 98 | full_tool_name = f"{prefix}{tool_name}" 99 | full_tool_desc = f"[{server_name}] {tool_desc}" if server_name else tool_desc 100 | 101 | try: 102 | # 非同期の MCP 呼び出し関数を定義 103 | async def call_mcp_tool_async(**kwargs: Any) -> Any: 104 | async with stdio_client(server_params) as (read, write): 105 | async with ClientSession(read, write) as session: 106 | await session.initialize() 107 | result = await session.call_tool(tool_name, arguments=kwargs) 108 | return result 109 | 110 | # 同期呼び出し用にラップする 111 | def tool_func(**kwargs: Any) -> Any: 112 | return asyncio.run(call_mcp_tool_async(**kwargs)) 113 | 114 | # StructuredToolを作成して返す 115 | return StructuredTool.from_function( 116 | func=tool_func, 117 | name=full_tool_name, 118 | description=full_tool_desc, 119 | args_schema=tool_item.inputSchema, 120 | ) 121 | except Exception as e: 122 | print(f"ツール '{full_tool_name}' の作成中にエラーが発生しました: {str(e)}") 123 | raise 124 | 125 | 126 | async def get_mcp_tools( 127 | session: ClientSession, server_name: Optional[str] 128 | ) -> List[Any]: 129 | """MCPセッションからツールリストを取得します""" 130 | try: 131 | print(f"サーバー '{server_name}' からツール一覧を取得しています...") 132 | response = await session.list_tools() 133 | return extract_tool_list(response) 134 | except Exception as e: 135 | print(f"ツール一覧取得中にエラーが発生しました: {e}") 136 | return [] 137 | 138 | 139 | async def load_mcp_tools( 140 | server_params: StdioServerParameters, server_name: Optional[str] = None 141 | ) -> List[StructuredTool]: 142 | """指定したMCPサーバーからツールをロードします""" 143 | tools: List[StructuredTool] = [] 144 | prefix = f"{server_name}__" if server_name else "" 145 | 146 | print(f"サーバー '{server_name}' に接続しています...") 147 | 148 | try: 149 | async with stdio_client(server_params) as (read, write): 150 | print(f"サーバー '{server_name}' への接続が確立されました") 151 | 152 | async with ClientSession(read, write) as session: 153 | await session.initialize() 154 | print(f"サーバー '{server_name}' のセッション初期化完了") 155 | 156 | # MCPサーバーが提供するツール一覧を取得 157 | tool_list = await get_mcp_tools(session, server_name) 158 | 159 | # 各ツールを処理 160 | processed_count = 0 161 | for tool_item in tool_list: 162 | try: 163 | # ツール名と説明を取得 164 | tool_name, tool_desc = extract_tool_info(tool_item) 165 | 166 | if not tool_name: 167 | continue 168 | 169 | print(f"ツール処理中: {tool_name}") 170 | 171 | # StructuredToolを作成 172 | lc_tool = await create_langchain_tool( 173 | tool_name, 174 | tool_desc, 175 | prefix, 176 | server_name, 177 | server_params, 178 | tool_item, 179 | ) 180 | tools.append(lc_tool) 181 | processed_count += 1 182 | print(f"ツール '{tool_name}' が正常に作成されました") 183 | except Exception as e: 184 | tool_name = getattr(tool_item, "name", str(tool_item)) 185 | print(f"ツール '{tool_name}' の作成に失敗: {str(e)}") 186 | 187 | print(f"処理したツール数: {processed_count}個") 188 | except Exception as e: 189 | print(f"サーバー '{server_name}' との通信に失敗: {e}") 190 | 191 | return tools 192 | 193 | 194 | async def load_all_mcp_tools( 195 | config: Optional[Dict[str, Any]] = None, 196 | ) -> List[StructuredTool]: 197 | """全てのMCPサーバーからツールをロードします""" 198 | if config is None: 199 | config = load_mcp_config("mcp_config.json") 200 | 201 | all_tools = [] 202 | server_params_dict = create_all_server_params(config) 203 | 204 | # 各サーバーのツールを取得して結合 205 | for server_name, params in server_params_dict.items(): 206 | print(f"\n--- サーバー '{server_name}' からツールをロード開始 ---") 207 | server_tools = await load_mcp_tools(params, server_name) 208 | all_tools.extend(server_tools) 209 | print( 210 | f"サーバー '{server_name}' から {len(server_tools)} 個のツールをロードしました" 211 | ) 212 | 213 | return all_tools 214 | 215 | 216 | if __name__ == "__main__": 217 | print("MCP Manager を起動しています...") 218 | try: 219 | config = load_mcp_config("mcp_config.json") 220 | # 利用可能なサーバー一覧を表示 221 | available_servers = get_available_servers(config) 222 | print(f"利用可能なMCPサーバー: {available_servers}") 223 | 224 | if len(available_servers) > 0: 225 | # すべてのサーバーからツールをロード 226 | print("すべてのサーバーからツールをロードしています...") 227 | all_tools = asyncio.run(load_all_mcp_tools(config)) 228 | print(f"全サーバーからロードされたツール合計: {len(all_tools)}個") 229 | 230 | # ツール一覧を表示 231 | for tool in all_tools: 232 | print(f"- {tool.name}: {tool.description}") 233 | else: 234 | print( 235 | "利用可能なサーバーが見つかりません。mcp_config.jsonを確認してください。" 236 | ) 237 | except Exception as e: 238 | print(f"エラーが発生しました: {e}") 239 | sys.exit(1) 240 | -------------------------------------------------------------------------------- /20/src/sd_20/prompts/system.txt: -------------------------------------------------------------------------------- 1 | # ナレッジエージェントの行動原理 2 | 3 | ## 1. 基本的な役割と目的 4 | 5 | あなたはWeb検索結果を収集し、分析し、データベースに体系的に保存するナレッジエージェントです。あなたの最大の目的は以下の通りです。 6 | 7 | - ユーザーの質問や関心事項に関連する最新で正確な情報をWeb上から収集すること 8 | - 収集した情報を整理・分類し、検索可能なナレッジベースとして構築すること 9 | - ユーザーの情報ニーズに応じて、保存された知識を効果的に活用・提供すること 10 | 11 | 現在の日付は{current_date}です。 12 | 13 | ## 2. 情報収集と処理のワークフロー 14 | 15 | ### 2.1 Web検索の実行 16 | 17 | - ユーザーの質問や指示に基づいて、明確で効果的な検索クエリを構築します 18 | - 曖昧な質問に対しては、より具体的な情報を求めることで検索精度を高めます 19 | - 技術的トピックには専門用語を、一般的な質問にはより広範な用語を使用します 20 | 21 | 例: 22 | ``` 23 | 「最新のAI規制」→「2025年 人工知能 法規制 最新動向」 24 | 「気候変動対策」→「気候変動 対策 国際協定 最新技術 削減方法」 25 | ``` 26 | 27 | ### 2.2 検索結果の評価と選択 28 | 29 | 検索結果の信頼性を以下の基準で評価します(0.0〜1.0のスコア): 30 | 31 | - 情報源の権威性: 政府機関、学術機関、専門家組織からの情報は高評価(0.8-1.0) 32 | - 最新性: 最新の情報ほど高評価、ただしトピックによって重要度は変化 33 | - 客観性: 事実に基づく情報は高評価、意見や偏りのある情報は低評価 34 | - 詳細度: 十分な背景情報と具体的なデータを含む情報は高評価 35 | - 一致性: 複数の信頼できる情報源で確認できる情報は高評価 36 | 37 | ### 2.3 情報の保存とタグ付け 38 | 39 | 情報をデータベースに保存する際は: 40 | 41 | - 要約: 要点を100〜200語で簡潔にまとめる 42 | - コンテンツタイプ分類: 「ニュース」「技術文書」「学術論文」「解説記事」など適切に分類 43 | - タグ付け: 主要キーワード、関連分野、時期などを含む3〜5個のタグを付与 44 | - 関連性の確保: ユーザーの元の質問やニーズに対する関連性を常に意識する 45 | 46 | ## 3. データベース活用の原則 47 | 48 | ### 3.1 既存情報の確認 49 | 50 | 新たな検索を行う前に、データベースに既に関連情報が存在するか確認します: 51 | 52 | 1. 関連するコンテンツタイプを特定 53 | 2. 最近の検索結果をチェック 54 | 3. 必要に応じてSQLクエリで詳細検索 55 | 56 | ### 3.2 情報の更新と拡充 57 | 58 | - 古くなった情報を特定し、最新の情報で更新 59 | - 不足している情報領域を特定し、能動的に補完 60 | - 関連する情報同士の接続性を高める 61 | 62 | ## 4. ユーザーとのインタラクション 63 | 64 | ### 4.1 情報提供の原則 65 | 66 | - 質問に対して最も関連性の高い情報を優先的に提供 67 | - 複雑な質問には段階的に回答し、理解度を確認 68 | - 情報の信頼性と限界について透明性を保つ 69 | 70 | ### 4.2 検索プロセスの共有 71 | 72 | - 使用した検索クエリと選択理由を共有 73 | - 評価した情報源とその選択基準を説明 74 | - 情報が不十分な場合、追加検索の方向性を提案 75 | 76 | --- 77 | 78 | # ツールの利用ガイド 79 | 80 | {tool_descriptions} 81 | 82 | # 出力形式 83 | 84 | 最終レポートは1000語以内でまとめてください。 85 | 必ず全ての出力が完了するようにしてください。 86 | 出力は常に日本語で行います。 -------------------------------------------------------------------------------- /20/src/sd_20/state.py: -------------------------------------------------------------------------------- 1 | from typing import Annotated, Sequence 2 | 3 | from langchain_core.messages import BaseMessage, trim_messages 4 | from langchain_core.messages.utils import count_tokens_approximately 5 | from langgraph.graph.message import Messages, add_messages 6 | from langgraph.managed import IsLastStep, RemainingSteps 7 | from typing_extensions import TypedDict 8 | 9 | MAX_TOKENS = 128_000 10 | 11 | def add_and_trim_messages( 12 | left_messages: Messages, 13 | right_messages: Messages 14 | ) -> Messages: 15 | """ 16 | メッセージを結合した後、指定されたトークン数に基づいてトリムします。 17 | 18 | Args: 19 | left_messages: ベースとなるメッセージリスト 20 | right_messages: 追加するメッセージリスト 21 | max_tokens: トリム後の最大トークン数 22 | token_counter: トークン数をカウントするLLM 23 | 24 | Returns: 25 | 結合・トリムされたメッセージリスト 26 | """ 27 | # メッセージを結合 28 | combined_messages = add_messages(left_messages, right_messages) 29 | 30 | # メッセージをトリム 31 | trimmed_messages = trim_messages( 32 | combined_messages, 33 | max_tokens=MAX_TOKENS, 34 | token_counter=count_tokens_approximately, 35 | strategy="last", # 最新のメッセージを保持 36 | include_system=True, # システムメッセージを保持 37 | ) 38 | 39 | return trimmed_messages 40 | 41 | class CustomAgentState(TypedDict): 42 | messages: Annotated[Sequence[BaseMessage], add_and_trim_messages] 43 | is_last_step: IsLastStep 44 | remaining_steps: RemainingSteps 45 | -------------------------------------------------------------------------------- /21/.env.example: -------------------------------------------------------------------------------- 1 | # Anthropic API Key 2 | ANTHROPIC_API_KEY=your_api_key_here -------------------------------------------------------------------------------- /21/.repomixignore: -------------------------------------------------------------------------------- 1 | # Add patterns to ignore here, one per line 2 | # Example: 3 | # *.log 4 | # tmp/ 5 | tmp/ 6 | .env 7 | uv.lock 8 | *.egg-info/ 9 | -------------------------------------------------------------------------------- /21/README.md: -------------------------------------------------------------------------------- 1 | # Software Design誌「実践LLMアプリケーション開発」第21回サンプルコード 2 | 3 | ## サンプルコードの実行方法 4 | 5 | ### 1. プロジェクトのセットアップ 6 | 7 | ※ このプロジェクトは`uv`を使用しています。`uv`のインストール方法については[こちら](https://github.com/astral-sh/uv)をご確認ください。 8 | 9 | 以下のコマンドを実行し、必要なライブラリのインストールを行って下さい。 10 | 11 | ``` 12 | $ uv sync 13 | ``` 14 | 15 | 次に環境変数の設定を行います。まず`.env.sample`ファイルをコピーして`.env`ファイルを作成します。 16 | 17 | ``` 18 | $ cp .env.sample .env 19 | $ vi .env # お好きなエディタで編集してください 20 | ``` 21 | 22 | `.env`ファイルを編集し、以下のAPIキーを設定してください。 23 | 24 | - `ANTHROPIC_API_KEY`: Claude APIのキー 25 | 26 | ## 実行方法 27 | 28 | ```bash 29 | uv run run.py 30 | ``` 31 | -------------------------------------------------------------------------------- /21/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "sd-21" 3 | version = "0.1.0" 4 | description = "LangGraph Functional API Sample" 5 | readme = "README.md" 6 | requires-python = ">=3.12" 7 | dependencies = [ 8 | "streamlit==1.44.1", 9 | "langchain-core==0.3.51", 10 | "langchain-anthropic==0.3.10", 11 | "langgraph==0.3.29", 12 | "python-dotenv==1.1.0", 13 | ] 14 | 15 | [project.scripts] 16 | content-creator = "src.main:main" 17 | 18 | [tool.setuptools] 19 | package-dir = {"" = "src"} 20 | 21 | [build-system] 22 | requires = ["setuptools>=61.0"] 23 | build-backend = "setuptools.build_meta" 24 | 25 | -------------------------------------------------------------------------------- /21/repomix.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "output": { 3 | "filePath": "tmp/repomix-output.txt", 4 | "style": "plain", 5 | "parsableStyle": false, 6 | "fileSummary": true, 7 | "directoryStructure": true, 8 | "removeComments": false, 9 | "removeEmptyLines": false, 10 | "compress": false, 11 | "topFilesLength": 5, 12 | "showLineNumbers": false, 13 | "copyToClipboard": false, 14 | "git": { 15 | "sortByChanges": true, 16 | "sortByChangesMaxCommits": 100 17 | } 18 | }, 19 | "include": [], 20 | "ignore": { 21 | "useGitignore": true, 22 | "useDefaultPatterns": true, 23 | "customPatterns": [] 24 | }, 25 | "security": { 26 | "enableSecurityCheck": true 27 | }, 28 | "tokenCount": { 29 | "encoding": "o200k_base" 30 | } 31 | } -------------------------------------------------------------------------------- /21/run.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | LangGraph Content Creator 実行スクリプト 4 | """ 5 | 6 | import os 7 | import sys 8 | 9 | import streamlit.web.cli as stcli 10 | from dotenv import load_dotenv 11 | 12 | 13 | def main(): 14 | """ 15 | アプリケーションを実行するエントリポイント 16 | """ 17 | # 環境変数の読み込み 18 | load_dotenv() 19 | 20 | if "ANTHROPIC_API_KEY" not in os.environ: 21 | print("エラー: ANTHROPIC_API_KEYが設定されていません。") 22 | print(".envファイルを作成し、APIキーを設定してください。") 23 | print("例: ANTHROPIC_API_KEY=your_api_key_here") 24 | sys.exit(1) 25 | 26 | # Streamlitアプリの実行 27 | app_path = os.path.join("src", "content_creator", "app.py") 28 | sys.argv = ["streamlit", "run", app_path] 29 | sys.exit(stcli.main()) 30 | 31 | 32 | if __name__ == "__main__": 33 | main() 34 | -------------------------------------------------------------------------------- /21/src/content_creator/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahm/softwaredesign-llm-application/a2ab09fde6c62bd538d4170ecc319000949ebfa0/21/src/content_creator/__init__.py -------------------------------------------------------------------------------- /21/src/content_creator/agent.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, List, Optional 2 | 3 | from langchain_anthropic import ChatAnthropic 4 | from langchain_core.messages import BaseMessage, HumanMessage 5 | from langchain_core.prompts import ChatPromptTemplate 6 | from langgraph.checkpoint.memory import MemorySaver 7 | from langgraph.func import entrypoint, task 8 | from pydantic import BaseModel 9 | 10 | # LLMの初期化 - Claude 3.7 Sonnet 11 | llm = ChatAnthropic( 12 | model_name="claude-3-7-sonnet-20250219", 13 | temperature=0.7, 14 | timeout=60, 15 | stop=None, 16 | ) 17 | 18 | 19 | @task 20 | def generate_content( 21 | theme: str, 22 | messages: Optional[List[BaseMessage]] = None, 23 | ) -> str: 24 | """指定されたユーザー入力とメッセージに基づいてコンテンツを生成する""" 25 | prompt = ChatPromptTemplate.from_messages( 26 | [ 27 | ( 28 | "system", 29 | "あなたはX(旧:Twitter)のポストを作成するエージェントです。ユーザーから与えられたテーマに基づいて、Xのポストを作成してください。曖昧な指示であっても、仮説ベースでコンテンツを作成するようにしてください。", 30 | ), 31 | ( 32 | "user", 33 | "次のテーマにしたがってポストを作成してください:{theme}{feedback_context}\n\nXに投稿するポストのみを出力すること。", 34 | ), 35 | ] 36 | ) 37 | 38 | # フィードバック履歴があれば追加 39 | feedback_context = "" 40 | if messages and len(messages) > 0: 41 | feedback_context = "\n\n以下はこれまでに受け取ったフィードバックです。これらすべてを考慮して改善してください:\n" 42 | for i, message in enumerate(messages, 1): 43 | # HumanMessageのみを抽出 44 | if isinstance(message, HumanMessage): 45 | feedback_context += f"{i}. {message.content}\n" 46 | elif ( 47 | isinstance(message, Dict) 48 | and "role" in message 49 | and message["role"] == "user" 50 | ): 51 | feedback_context += f"{i}. {message['content']}\n" 52 | 53 | # LLMを呼び出してコンテンツを生成 54 | chain = prompt | llm 55 | response = chain.invoke( 56 | { 57 | "theme": theme, 58 | "feedback_context": feedback_context, 59 | } 60 | ) 61 | return str(response.content) 62 | 63 | 64 | class FeedbackList(BaseModel): 65 | values: List[str] 66 | 67 | 68 | @task 69 | def generate_feedback_options(content: str) -> List[str]: 70 | """コンテンツに対するフィードバック候補を生成する""" 71 | prompt = ChatPromptTemplate.from_messages( 72 | [ 73 | ( 74 | "system", 75 | "あなたはコンテンツをより魅力的なものにするためのフィードバック候補を生成するアシスタントです。", 76 | ), 77 | ( 78 | "user", 79 | "以下のX(旧:Twitter)向けのポストに対する3つの異なるフィードバック候補を生成してください。それぞれ異なる観点から改善点を指摘してください。\n\n重要: 各フィードバックは20文字以内の簡潔な指示文にしてください。ボタンラベルとしても使用できる長さが必要です。\n\nコンテンツ:\n{content}", 80 | ), 81 | ] 82 | ) 83 | chain = prompt | llm.with_structured_output(FeedbackList) 84 | response: FeedbackList = chain.invoke({"content": content}) # type: ignore 85 | 86 | return response.values 87 | 88 | 89 | @entrypoint(checkpointer=MemorySaver()) 90 | def workflow( 91 | inputs: Dict[str, Any], 92 | *, 93 | previous: Optional[Dict[str, Any]] = None, 94 | ) -> Dict[str, Any]: 95 | # 以前の状態を取得または初期化 96 | state = previous or { 97 | "theme": "", 98 | "content": "", 99 | "options": [], 100 | "messages": [], 101 | } 102 | 103 | # 最初の指示をコンテンツのテーマとして設定する 104 | if state["theme"] == "": 105 | state["theme"] = inputs["user_input"] 106 | 107 | # ユーザーの指示をメッセージに追加 108 | state["messages"].append({"role": "user", "content": inputs["user_input"]}) 109 | 110 | # コンテンツを生成 111 | content = generate_content(state["theme"], state["messages"]).result() 112 | state["content"] = content 113 | 114 | # フィードバックオプションを生成 115 | feedback_options = generate_feedback_options(content).result() 116 | state["options"] = feedback_options 117 | 118 | # アシスタントメッセージを追加 119 | message = "コンテンツを生成しました。フィードバックをお願いします。" 120 | state["messages"].append({"role": "assistant", "content": message}) 121 | 122 | # 最終的なステートを返し、チェックポイントに保存 123 | return state 124 | -------------------------------------------------------------------------------- /21/src/content_creator/app.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | import streamlit as st 4 | 5 | from content_creator.agent import workflow 6 | from content_creator.ui_components import ( 7 | render_chat_input, 8 | render_content_area, 9 | render_feedback_options, 10 | render_messages, 11 | render_sidebar, 12 | setup_page_config, 13 | ) 14 | 15 | 16 | def main(): 17 | """メインアプリケーション関数""" 18 | # ページ設定 19 | setup_page_config() 20 | 21 | if "thread_id" not in st.session_state: 22 | st.session_state.thread_id = str(uuid.uuid4()) 23 | if "workflow_state" not in st.session_state: 24 | st.session_state.workflow_state = "idle" # idle, feedback 25 | if "debug_info" not in st.session_state: 26 | st.session_state.debug_info = {} 27 | if "current_data" not in st.session_state: 28 | st.session_state.current_data = {} 29 | 30 | # サイドバーは最初に表示 31 | render_sidebar() 32 | 33 | # レイアウト - 2カラムレイアウト 34 | col1, col2 = st.columns([1, 1]) 35 | 36 | # 左カラム - チャット欄 37 | with col1: 38 | st.subheader("チャット") 39 | 40 | # チャット履歴(LangGraphから取得) 41 | if "current_data" in st.session_state and st.session_state.current_data: 42 | render_messages(st.session_state.current_data.get("messages", [])) 43 | 44 | # フィードバックオプション - チャット欄に表示 45 | if st.session_state.workflow_state == "feedback": 46 | # フィードバックの選択肢を取得 47 | feedback_options = st.session_state.current_data.get("options", []) 48 | 49 | # フィードバック入力欄の表示 50 | feedback = render_feedback_options(feedback_options) 51 | 52 | # フィードバックが提供された場合 53 | if feedback: 54 | with st.spinner("フィードバックを反映中..."): 55 | # ワークフローを再開 56 | process_workflow(feedback) 57 | 58 | # フィードバック待ちでない場合のみ入力フィールドを表示 59 | if st.session_state.workflow_state != "feedback": 60 | # 新しい指示の入力 61 | prompt = render_chat_input() 62 | 63 | # ユーザーからの指示が入力された場合 64 | if prompt: 65 | with st.spinner("コンテンツを生成中..."): 66 | # ワークフロー実行 - 新しいプロンプトを入力 67 | process_workflow(prompt) 68 | 69 | # 右カラム - コンテンツ表示 70 | with col2: 71 | st.subheader("生成されたコンテンツ") 72 | 73 | # コンテンツ表示 74 | if "current_data" in st.session_state and st.session_state.current_data: 75 | content = st.session_state.current_data.get("content", "") 76 | 77 | if content: 78 | # コンテンツの内容をレンダリング 79 | render_content_area(content) 80 | 81 | # ステータス情報(デバッグ用) 82 | with st.expander("デバッグ情報", expanded=False): 83 | st.write(f"ステータス: {st.session_state.workflow_state}") 84 | if st.session_state.debug_info: 85 | st.json(st.session_state.debug_info) 86 | else: 87 | st.info("チャット欄に指示を入力すると、ここにコンテンツが表示されます。") 88 | 89 | 90 | def process_workflow(user_input: str): 91 | """ 92 | ワークフローを実行する 93 | 94 | Args: 95 | user_input: ユーザーからの入力 96 | """ 97 | # ユーザー入力 98 | input_data = {"user_input": user_input} 99 | 100 | # スレッドIDの設定 101 | config = {"configurable": {"thread_id": st.session_state.thread_id}} 102 | 103 | # ワークフロー実行 104 | for chunk in workflow.stream( 105 | input=input_data, 106 | config=config, 107 | ): 108 | if "workflow" in chunk: 109 | # デバッグ情報にinterrupt_dataを保存 110 | workflow_data = chunk["workflow"] 111 | st.session_state.debug_info["interrupt_data"] = workflow_data 112 | 113 | # ワークフロー状態をフィードバック待ちに設定 114 | st.session_state.workflow_state = "feedback" 115 | 116 | # 抽出したデータをUIで利用できるように保存 117 | st.session_state.current_data = workflow_data 118 | 119 | # 全処理完了後にrerun()を実行 120 | st.rerun() 121 | 122 | 123 | if __name__ == "__main__": 124 | main() 125 | -------------------------------------------------------------------------------- /21/src/content_creator/ui_components.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, List 2 | 3 | import streamlit as st 4 | 5 | 6 | def setup_page_config(): 7 | """Streamlitページ設定""" 8 | st.set_page_config( 9 | page_title="Content Creator", 10 | page_icon="📝", 11 | layout="wide", 12 | ) 13 | 14 | # 全体的なパネルスタイルのCSSを定義 15 | st.markdown( 16 | """ 17 | 62 | """, 63 | unsafe_allow_html=True, 64 | ) 65 | 66 | 67 | def render_sidebar(): 68 | """サイドバーのレンダリング""" 69 | with st.sidebar: 70 | st.title("Content Creator") 71 | st.markdown( 72 | "このアプリは、LangGraph Functional APIを使用して、ユーザーの指示に基づいてコンテンツを生成します。" 73 | ) 74 | st.markdown("---") 75 | st.markdown("### 使い方") 76 | st.markdown("1. 左側のチャット欄に指示を入力します") 77 | st.markdown("2. 右側に生成されたコンテンツが表示されます") 78 | st.markdown("3. フィードバックを選択するか自由に入力して改善できます") 79 | 80 | # ステータス表示 81 | st.markdown("---") 82 | st.markdown("### ステータス") 83 | if "workflow_state" in st.session_state: 84 | if st.session_state.workflow_state == "idle": 85 | st.success("準備完了") 86 | elif st.session_state.workflow_state == "feedback": 87 | st.warning("フィードバック待ち") 88 | 89 | 90 | def render_chat_input(): 91 | """チャット入力エリアのレンダリング""" 92 | prompt = st.chat_input("指示を入力してください...") 93 | return prompt 94 | 95 | 96 | def render_messages(messages: List[Dict[str, Any]]): 97 | """チャット履歴のレンダリング""" 98 | for message in messages: 99 | try: 100 | role = message["role"] 101 | content = message["content"] 102 | 103 | # チャットメッセージを表示 104 | with st.chat_message(role): 105 | st.write(content) 106 | except Exception as e: 107 | # メッセージ表示に失敗した場合は警告 108 | with st.chat_message("system"): 109 | st.warning(f"メッセージの表示に失敗しました: {str(e)}") 110 | 111 | 112 | def render_content_area(content: str): 113 | """コンテンツ表示エリアのレンダリング - 右カラムでの表示に最適化""" 114 | with st.container(): 115 | # コンテンツが存在する場合は文字数を表示 116 | if content: 117 | # HTMLタグを除いた純粋なテキストの文字数をカウント 118 | char_count = len(content) 119 | st.markdown( 120 | f'
{char_count}文字
', 121 | unsafe_allow_html=True, 122 | ) 123 | 124 | st.markdown(f'
{content}
', unsafe_allow_html=True) 125 | 126 | 127 | def render_feedback_options(options: List[str]): 128 | """フィードバックオプションのレンダリング""" 129 | with st.container(border=True): 130 | opt_cols = st.columns(3) 131 | for i, option in enumerate(options[:3]): # 最大3つまで表示 132 | with opt_cols[i]: 133 | if st.button( 134 | option.strip(), 135 | key=f"feedback_opt_{i}", 136 | type="primary" if i == 0 else "secondary", 137 | use_container_width=True, 138 | ): 139 | return option 140 | 141 | # 自由入力フィールド 142 | st.caption("または、自由にフィードバックを入力:") 143 | 144 | # フォームを使用して一度だけ送信されるようにする 145 | with st.form(key="feedback_form", clear_on_submit=True): 146 | custom_feedback = st.text_input( 147 | "フィードバック", key="custom_feedback", label_visibility="collapsed" 148 | ) 149 | submitted = st.form_submit_button("送信") 150 | 151 | if submitted and custom_feedback.strip(): 152 | return custom_feedback.strip() 153 | -------------------------------------------------------------------------------- /21/src/main.py: -------------------------------------------------------------------------------- 1 | """ 2 | LangGraph Content Creator - アプリケーションエントリポイント 3 | 4 | Streamlitを起動して、LangGraphによるコンテンツ生成アプリを実行します。 5 | """ 6 | 7 | import os 8 | import sys 9 | 10 | import streamlit.web.cli as stcli 11 | from dotenv import load_dotenv 12 | 13 | 14 | def main(): 15 | """アプリケーションのエントリポイント""" 16 | # 環境変数の読み込み 17 | load_dotenv() 18 | 19 | # Streamlitに渡す引数を構築 20 | app_path = os.path.join(os.path.dirname(__file__), "content_creator/app.py") 21 | sys.argv = ["streamlit", "run", app_path, "--server.runOnSave=false"] 22 | 23 | # Streamlitの実行 24 | sys.exit(stcli.main()) 25 | 26 | 27 | if __name__ == "__main__": 28 | main() 29 | -------------------------------------------------------------------------------- /22/.python-version: -------------------------------------------------------------------------------- 1 | 3.12 2 | -------------------------------------------------------------------------------- /22/README.md: -------------------------------------------------------------------------------- 1 | # Software Design誌「実践LLMアプリケーション開発」第22回サンプルコード 2 | 3 | ## サンプルコードの実行方法 4 | 5 | ### プロジェクトのセットアップ 6 | 7 | ※ このプロジェクトは`uv`を使用しています。`uv`のインストール方法については[こちら](https://github.com/astral-sh/uv)をご確認ください。 8 | 9 | 以下のコマンドを実行し、必要なライブラリのインストールを行って下さい。 10 | 11 | ``` 12 | $ uv sync 13 | ``` 14 | 15 | 次に環境変数の設定を行います。まず`.env.sample`ファイルをコピーして`.env`ファイルを作成します。 16 | 17 | ``` 18 | $ cp .env.sample .env 19 | $ vi .env # お好きなエディタで編集してください 20 | ``` 21 | 22 | `.env`ファイルを編集し、以下のAPIキーを設定してください。 23 | 24 | - `ANTHROPIC_API_KEY`: Claude APIのキー 25 | 26 | ### 実行方法 27 | 28 | ```bash 29 | uv run run.py 30 | ``` 31 | 32 | ## Streamlitアプリとエージェントとの通信の全体像 33 | 34 | Streamlitアプリとエージェントとの通信の全体像は次の図の通りです。コード理解の際にお役立てください。 35 | 36 | ```mermaid 37 | sequenceDiagram 38 | participant User as ユーザー 39 | participant UI as Streamlit UI 40 | participant Agent as 領収書OCRエージェント 41 | participant Tasks as タスク (OCR/会計処理) 42 | 43 | note over UI: 初期状態: WorkflowState.INITIAL 44 | 45 | User->>UI: 画像アップロード 46 | User->>UI: 「処理開始」ボタンクリック 47 | 48 | note over UI: 状態変更: WorkflowState.PROCESSING 49 | 50 | UI->>Agent: receipt_workflow(image_path, thread_id=X) 51 | 52 | Agent->>Tasks: process_and_ocr_image() 53 | Tasks-->>Agent: OCR結果 54 | 55 | %% OCRイベントのストリーミング 56 | Agent-->>UI: StreamWriter: OCR_DONE イベント 57 | 58 | note over UI: UI更新: OCR結果を保存 59 | note over UI: st.session_state.ocr_text, st.session_state.ocr_result を更新 60 | 61 | Agent->>Tasks: generate_account_suggestion() 62 | Tasks-->>Agent: 勘定科目提案 63 | 64 | %% 提案イベントのストリーミング 65 | Agent-->>UI: StreamWriter: ACCOUNT_SUGGESTED イベント 66 | 67 | note over UI: UI更新: 会計情報を保存 68 | note over UI: st.session_state.account_info を更新 69 | 70 | %% interruptによる一時停止 71 | Agent-->>UI: interrupt() - 割り込み情報 72 | 73 | note over UI: 状態変更: WorkflowState.WAIT_FEEDBACK 74 | note over UI: st.rerun() で画面更新 75 | 76 | %% ユーザーフィードバックの待機状態 77 | UI-->>User: OCR結果と勘定科目提案を表示 78 | UI-->>User: フィードバック入力フォームを表示 79 | 80 | alt ユーザーが修正を要求 81 | User->>UI: フィードバック入力 + 「再生成」ボタンクリック 82 | 83 | note over UI: 状態変更: WorkflowState.PROCESSING 84 | 85 | UI->>Agent: Command(resume=Feedback(REGENERATE, text)) 86 | 87 | %% タスクの再実行(OCRはキャッシュから) 88 | Agent->>Tasks: process_and_ocr_image() (キャッシュから) 89 | Tasks-->>Agent: キャッシュされたOCR結果 90 | 91 | Agent->>Tasks: generate_account_suggestion(feedback_history=[text]) 92 | Tasks-->>Agent: 更新された勘定科目提案 93 | 94 | %% 再び割り込み 95 | Agent-->>UI: interrupt() - 更新された提案情報 96 | 97 | note over UI: 状態変更: WorkflowState.WAIT_FEEDBACK 98 | note over UI: UI更新: st.session_state.account_info を更新 99 | note over UI: st.rerun() で画面更新 100 | 101 | UI-->>User: 更新された勘定科目提案を表示 102 | else ユーザーが承認 103 | User->>UI: 「承認」ボタンクリック 104 | 105 | note over UI: 状態変更: WorkflowState.PROCESSING 106 | 107 | UI->>Agent: Command(resume=Feedback(APPROVE, "")) 108 | 109 | Agent->>Tasks: save_receipt_data() 110 | Tasks-->>Agent: 保存結果 111 | 112 | %% 保存完了イベント 113 | Agent-->>UI: StreamWriter: SAVE_COMPLETED イベント 114 | 115 | note over UI: 状態変更: WorkflowState.WORKFLOW_COMPLETED 116 | note over UI: st.rerun() で画面更新 117 | 118 | UI-->>User: 完了メッセージを表示 119 | end 120 | ``` -------------------------------------------------------------------------------- /22/fixtures/receipt_meeting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahm/softwaredesign-llm-application/a2ab09fde6c62bd538d4170ecc319000949ebfa0/22/fixtures/receipt_meeting.png -------------------------------------------------------------------------------- /22/fixtures/receipt_parking.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mahm/softwaredesign-llm-application/a2ab09fde6c62bd538d4170ecc319000949ebfa0/22/fixtures/receipt_parking.png -------------------------------------------------------------------------------- /22/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=42", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "receipt-ocr-agent" 7 | version = "0.1.0" 8 | description = "領収書OCRエージェント" 9 | authors = [ 10 | {name = "Your Name", email = "your.email@example.com"}, 11 | ] 12 | requires-python = ">=3.12" 13 | dependencies = [ 14 | "streamlit>=1.45.0", 15 | "langchain-core>=0.3.58", 16 | "langchain-anthropic>=0.3.12", 17 | "langgraph>=0.4.1", 18 | "python-dotenv>=1.1.0", 19 | "pillow>=11.2.1", 20 | "pandas>=2.2.3", 21 | "pydantic>=2.11.4", 22 | ] 23 | 24 | [tool.uv] 25 | dev-dependencies = [ 26 | "pandas-stubs>=2.2.3.250308", 27 | "black>=25.1.0", 28 | "isort>=6.0.1", 29 | "mypy>=1.15.0", 30 | ] 31 | 32 | [tool.setuptools] 33 | packages = ["src"] 34 | 35 | [tool.black] 36 | line-length = 100 37 | target-version = ["py312"] 38 | 39 | [tool.isort] 40 | profile = "black" 41 | line_length = 100 42 | 43 | [tool.mypy] 44 | python_version = "3.12" 45 | warn_return_any = true 46 | warn_unused_configs = true 47 | disallow_untyped_defs = true 48 | disallow_incomplete_defs = true 49 | -------------------------------------------------------------------------------- /22/repomix.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "output": { 3 | "filePath": "tmp/repomix-output.txt", 4 | "style": "plain", 5 | "parsableStyle": false, 6 | "fileSummary": true, 7 | "directoryStructure": true, 8 | "removeComments": false, 9 | "removeEmptyLines": false, 10 | "compress": false, 11 | "topFilesLength": 5, 12 | "showLineNumbers": false, 13 | "copyToClipboard": false, 14 | "git": { 15 | "sortByChanges": true, 16 | "sortByChangesMaxCommits": 100 17 | } 18 | }, 19 | "include": [], 20 | "ignore": { 21 | "useGitignore": true, 22 | "useDefaultPatterns": true, 23 | "customPatterns": [] 24 | }, 25 | "security": { 26 | "enableSecurityCheck": true 27 | }, 28 | "tokenCount": { 29 | "encoding": "o200k_base" 30 | } 31 | } -------------------------------------------------------------------------------- /22/run.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | 領収書OCRエージェント実行スクリプト 4 | """ 5 | import os 6 | import sys 7 | from pathlib import Path 8 | 9 | import streamlit.web.cli as stcli 10 | from dotenv import load_dotenv 11 | 12 | 13 | def main() -> None: 14 | """アプリケーションのメイン関数""" 15 | # 環境変数の読み込み 16 | env_path = Path(".") / ".env" 17 | load_dotenv(dotenv_path=env_path) 18 | 19 | # 環境変数チェック 20 | required_vars = ["ANTHROPIC_API_KEY"] 21 | missing_vars = [var for var in required_vars if not os.environ.get(var)] 22 | if missing_vars: 23 | print(f"エラー: 以下の環境変数が設定されていません: {', '.join(missing_vars)}") 24 | print("実行前に.envファイルを作成するか、環境変数を設定してください。") 25 | sys.exit(1) 26 | 27 | # プロジェクトルートディレクトリをPYTHONPATHに追加 28 | project_root = Path(__file__).parent.absolute() 29 | sys.path.insert(0, str(project_root)) 30 | 31 | # tmp ディレクトリがない場合は作成 32 | tmp_dir = project_root / "tmp" 33 | if not tmp_dir.exists(): 34 | tmp_dir.mkdir(parents=True) 35 | 36 | # アプリケーションファイルへのパス(直接ファイルパスを指定) 37 | app_path = str(project_root / "src" / "receipt_processor" / "app.py") 38 | 39 | # Streamlitを直接実行(ファイルパスを指定して実行) 40 | sys.argv = [ 41 | "streamlit", 42 | "run", 43 | app_path, 44 | "--server.port=8501", 45 | "--server.address=localhost", 46 | ] 47 | sys.exit(stcli.main()) 48 | 49 | 50 | if __name__ == "__main__": 51 | main() 52 | -------------------------------------------------------------------------------- /22/src/receipt_processor/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 領収書OCRエージェントモジュール 3 | """ 4 | -------------------------------------------------------------------------------- /22/src/receipt_processor/account.py: -------------------------------------------------------------------------------- 1 | """ 2 | 勘定科目提案機能 3 | """ 4 | 5 | from typing import Any, Dict 6 | 7 | from langchain_anthropic import ChatAnthropic 8 | from langchain_core.prompts import ChatPromptTemplate 9 | from langchain_core.runnables import RunnableLambda 10 | 11 | from src.receipt_processor.constants import CLAUDE_SMART_MODEL 12 | from src.receipt_processor.models import AccountInfo, ReceiptOCRResult 13 | 14 | 15 | def format_prompt(inputs: Dict[str, Any]) -> ChatPromptTemplate: 16 | """ 17 | OCR結果からプロンプトテンプレートを生成する 18 | 19 | Parameters: 20 | ----------- 21 | inputs: Dict[str, Any] 22 | OCR結果とフィードバック情報を含む辞書 23 | 24 | Returns: 25 | -------- 26 | ChatPromptTemplate 27 | 適切に設定されたChatPromptTemplate 28 | """ 29 | ocr_result = inputs["ocr_result"] 30 | feedback = inputs.get("feedback", "") 31 | 32 | # 品目情報のテキスト化 33 | items_text = "" 34 | if ocr_result.items: 35 | items_text = "品目リスト:\n" 36 | for item in ocr_result.items: 37 | items_text += f"- {item.name}: {item.price}円\n" 38 | 39 | # その他情報のテキスト化 40 | other_info_text = "" 41 | if ocr_result.other_info: 42 | other_info_text = "その他情報:\n" 43 | for info in ocr_result.other_info: 44 | other_info_text += f"- {info.key}: {info.value}\n" 45 | 46 | # フィードバックセクションの制御 47 | feedback_section = "" 48 | if feedback: 49 | feedback_section = f"""\ 50 | 【ユーザーフィードバック】 51 | {feedback} 52 | """ 53 | 54 | # システムプロンプト 55 | system_prompt = """\ 56 | あなたは日本の会計士です。領収書の情報から最適な勘定科目を提案してください。 57 | 中小企業の一般的な勘定科目を使用し、適切な補助科目、取引先情報、摘要も含めてください。 58 | なぜその勘定科目が適切かの理由も必ず含めてください。 59 | 60 | 使用可能な主な勘定科目の例: 61 | - 旅費交通費: 交通機関の利用料、出張費など 62 | - 通信費: 電話料金、インターネット料金など 63 | - 消耗品費: 事務用品、日用品など 64 | - 会議費: 会議での飲食代など 65 | - 接待交際費: 取引先との会食、贈答品など 66 | - 広告宣伝費: 広告費、販促物など 67 | - 新聞図書費: 書籍、雑誌、新聞代など 68 | - 水道光熱費: 電気代、ガス代、水道代など 69 | - 地代家賃: オフィス賃料など 70 | - 雑費: 他の科目に当てはまらない少額の経費 71 | """.strip() 72 | 73 | # ユーザープロンプト 74 | user_prompt = f"""\ 75 | 以下の領収書情報から、最適な勘定科目情報を提案してください。 76 | 77 | 【領収書情報】 78 | 日付: {ocr_result.date} 79 | 金額: {ocr_result.amount}円 80 | 店舗/発行者: {ocr_result.shop_name} 81 | {items_text} 82 | {other_info_text} 83 | 詳細: {ocr_result.raw_text} 84 | {feedback_section} 85 | """.strip() 86 | 87 | # ChatPromptTemplateを作成して返す 88 | return ChatPromptTemplate.from_messages( 89 | [ 90 | ("system", system_prompt), 91 | ("user", user_prompt), 92 | ] 93 | ) 94 | 95 | 96 | def suggest_account_info( 97 | ocr_result: ReceiptOCRResult, 98 | feedback: str | None = None, 99 | model_name: str = CLAUDE_SMART_MODEL, 100 | ) -> AccountInfo: 101 | """ 102 | OCR結果から適切な勘定科目情報を提案する 103 | 104 | Parameters: 105 | ----------- 106 | ocr_result: ReceiptOCRResult 107 | OCR処理結果の構造化データ 108 | feedback: str | None 109 | ユーザーからのフィードバック(あれば) 110 | model_name: str 111 | 使用するClaudeモデル名 112 | 113 | Returns: 114 | -------- 115 | AccountInfo 116 | 提案された勘定科目情報 117 | """ 118 | # LLMの初期化 119 | llm = ChatAnthropic( 120 | model_name=model_name, 121 | temperature=0.2, 122 | timeout=None, 123 | stop=None, 124 | max_retries=3, 125 | ) 126 | 127 | # プロンプト生成用のRunnableLambda 128 | prompt_generator = RunnableLambda(format_prompt) 129 | 130 | # チェーンの構築 131 | account_chain = prompt_generator | llm.with_structured_output( 132 | AccountInfo, 133 | mode="function_calling", 134 | ) 135 | 136 | # 勘定科目情報を生成 137 | account_info: AccountInfo = account_chain.invoke( 138 | {"ocr_result": ocr_result, "feedback": feedback} 139 | ) # type: ignore 140 | 141 | return account_info 142 | -------------------------------------------------------------------------------- /22/src/receipt_processor/agent.py: -------------------------------------------------------------------------------- 1 | """ 2 | LangGraph Workflowの定義 3 | """ 4 | 5 | import os 6 | from typing import Any, Dict, List, Optional 7 | 8 | from langgraph.checkpoint.memory import MemorySaver 9 | from langgraph.func import entrypoint, task 10 | from langgraph.types import StreamWriter, interrupt 11 | 12 | from src.receipt_processor.account import suggest_account_info 13 | from src.receipt_processor.constants import CSV_FILE_PATH 14 | from src.receipt_processor.models import ( 15 | AccountInfo, 16 | CommandType, 17 | EventType, 18 | Feedback, 19 | ReceiptOCRResult, 20 | ) 21 | from src.receipt_processor.storage import backup_csv, save_to_csv 22 | from src.receipt_processor.vision import ocr_receipt 23 | 24 | 25 | @task 26 | def process_and_ocr_image(image_path: str, *, writer: StreamWriter) -> ReceiptOCRResult: 27 | """ 28 | 画像の前処理とOCR処理を行う統合タスク 29 | 30 | Parameters: 31 | ----------- 32 | image_path: str 33 | 処理する画像のファイルパス 34 | writer: StreamWriter 35 | イベント送信用のStreamWriter 36 | 37 | Returns: 38 | -------- 39 | ReceiptOCRResult 40 | 抽出された領収書情報(構造化データ) 41 | """ 42 | # 画像パスが存在するかチェック 43 | if not os.path.exists(image_path): 44 | raise FileNotFoundError(f"画像ファイルが見つかりません: {image_path}") 45 | 46 | # OCR処理を実行 47 | ocr_result = ocr_receipt(image_path) 48 | 49 | # OCR完了イベントを送信 50 | writer( 51 | { 52 | "event": EventType.OCR_DONE, 53 | "text": ocr_result.raw_text, 54 | "structured_data": ocr_result.model_dump(), 55 | } 56 | ) 57 | 58 | return ocr_result 59 | 60 | 61 | @task 62 | def generate_account_suggestion( 63 | ocr_result: ReceiptOCRResult, 64 | feedback_history: Optional[List[str]] = None, 65 | *, 66 | writer: StreamWriter, 67 | ) -> AccountInfo: 68 | """ 69 | OCR結果から勘定科目情報を提案するタスク 70 | 71 | Parameters: 72 | ----------- 73 | ocr_result: ReceiptOCRResult 74 | OCR処理結果の構造化データ 75 | feedback_history: Optional[List[str]] 76 | これまでのユーザーフィードバック履歴 77 | writer: StreamWriter 78 | イベント送信用のStreamWriter 79 | 80 | Returns: 81 | -------- 82 | AccountInfo 83 | 提案された勘定科目情報 84 | """ 85 | # フィードバック履歴がある場合はフィードバックとして使用 86 | combined_feedback = None 87 | if feedback_history and len(feedback_history) > 0: 88 | # すべてのフィードバックを結合して渡す 89 | combined_feedback = " ".join( 90 | [f"フィードバック{i+1}: {fb}" for i, fb in enumerate(feedback_history)] 91 | ) 92 | 93 | # 勘定科目提案を取得 94 | account_info = suggest_account_info(ocr_result, combined_feedback) 95 | 96 | # 提案完了イベントを送信 97 | writer( 98 | { 99 | "event": EventType.ACCOUNT_SUGGESTED, 100 | "account_info": account_info.model_dump(), 101 | "ocr_text": ocr_result.raw_text, 102 | } 103 | ) 104 | 105 | return account_info 106 | 107 | 108 | @task 109 | def save_receipt_data(data: AccountInfo, *, writer: StreamWriter) -> bool: 110 | """ 111 | 承認されたデータをCSVに保存するタスク 112 | 113 | Parameters: 114 | ----------- 115 | data: AccountInfo 116 | 保存するデータ 117 | writer: StreamWriter 118 | イベント送信用のStreamWriter 119 | 120 | Returns: 121 | -------- 122 | bool 123 | 保存が成功したかどうか 124 | """ 125 | # CSVファイルが既に存在する場合はバックアップを作成 126 | if os.path.exists(CSV_FILE_PATH): 127 | backup_csv() 128 | 129 | # データをCSVに保存 130 | save_success = save_to_csv(data) 131 | 132 | # 保存完了イベントを送信 133 | writer( 134 | { 135 | "event": EventType.SAVE_COMPLETED, 136 | "account_info": data.model_dump(), 137 | } 138 | ) 139 | 140 | return save_success 141 | 142 | 143 | @entrypoint(checkpointer=MemorySaver()) 144 | def receipt_workflow( 145 | image_path: str, 146 | *, 147 | previous: Any = None, 148 | writer: StreamWriter, 149 | ) -> Dict[str, Any]: 150 | """ 151 | 領収書OCRと勘定科目提案のワークフロー 152 | 153 | Parameters: 154 | ----------- 155 | image_path: str 156 | 処理する画像のファイルパス 157 | previous: Any 158 | 前回の状態データ 159 | writer: StreamWriter 160 | イベント送信用のStreamWriter 161 | 162 | Returns: 163 | -------- 164 | Dict[str, Any] 165 | 更新された状態 166 | """ 167 | # 状態の初期化または復元 168 | state = previous or { 169 | "image_path": image_path, # 処理中の画像ファイルパス 170 | "feedback_history": [], # ユーザーからのフィードバック履歴 171 | "completed": False, # ワークフロー完了フラグ 172 | } 173 | 174 | # 画像パスが変更された場合は更新 175 | if image_path != state.get("image_path", ""): 176 | state["image_path"] = image_path 177 | # 新しい画像の場合はフィードバック履歴をリセット 178 | state["feedback_history"] = [] 179 | 180 | # OCRを実行して結果を保存 181 | ocr_result = process_and_ocr_image(image_path, writer=writer).result() 182 | 183 | # 勘定科目提案を取得(フィードバック履歴があれば利用) 184 | account_info = generate_account_suggestion( 185 | ocr_result, feedback_history=state.get("feedback_history", []), writer=writer 186 | ).result() 187 | 188 | while True: 189 | # ユーザーからのフィードバックを待機 190 | response = interrupt( 191 | { 192 | "ocr_result": ocr_result.model_dump(), 193 | "account_info": account_info.model_dump(), 194 | "feedback_count": len(state.get("feedback_history", [])), 195 | } 196 | ) 197 | 198 | feedback: Feedback = Feedback.model_validate(response) 199 | 200 | # 承認コマンドの場合 201 | if feedback.command == CommandType.APPROVE: 202 | # CSVに保存 203 | save_receipt_data(account_info, writer=writer).result() 204 | 205 | # 状態を更新して完了とマーク 206 | state.update( 207 | { 208 | "ocr_result": ocr_result.model_dump(), 209 | "account_info": account_info.model_dump(), 210 | "completed": True, 211 | } 212 | ) 213 | 214 | break 215 | 216 | # フィードバックを受け取った場合 217 | elif feedback.command == CommandType.REGENERATE: 218 | feedback_content = feedback.content 219 | 220 | if feedback_content: 221 | # フィードバック履歴に追加 222 | state["feedback_history"] = state.get("feedback_history", []) + [ 223 | feedback_content 224 | ] 225 | # フィードバックを使って勘定科目を再提案 226 | account_info = generate_account_suggestion( 227 | ocr_result, 228 | feedback_history=state["feedback_history"], 229 | writer=writer, 230 | ).result() 231 | 232 | # 未知のコマンドが渡された場合 233 | else: 234 | # エラーイベントを送信 235 | writer( 236 | { 237 | "event": EventType.ERROR, 238 | "message": f"不正なコマンドです: {feedback.command}。'approve'または'regenerate'を使用してください。", 239 | "command": feedback.command, 240 | } 241 | ) 242 | 243 | return state 244 | -------------------------------------------------------------------------------- /22/src/receipt_processor/constants.py: -------------------------------------------------------------------------------- 1 | """ 2 | アプリケーション全体で使用する定数の定義 3 | """ 4 | 5 | # ファイルパス関連 6 | CSV_FILE_PATH = "tmp/db.csv" 7 | 8 | # LLM関連 9 | CLAUDE_FAST_MODEL = "claude-3-5-haiku-20241022" 10 | CLAUDE_SMART_MODEL = "claude-3-7-sonnet-20250219" 11 | -------------------------------------------------------------------------------- /22/src/receipt_processor/models.py: -------------------------------------------------------------------------------- 1 | """ 2 | データモデル定義 3 | """ 4 | 5 | from enum import Enum 6 | from typing import List 7 | 8 | from pydantic import BaseModel, Field 9 | 10 | 11 | class WorkflowState(str, Enum): 12 | """ワークフローの状態定義(UI)""" 13 | 14 | IDLE = "idle" # 初期状態、ユーザーがアクション(画像アップロードなど)を実行する前の待機状態 15 | PROCESSING = "processing" # データ処理中の状態(OCR処理や勘定科目の判定中など) 16 | WAIT_FEEDBACK = "feedback" # ユーザーからのフィードバック入力待ち状態 17 | WORKFLOW_COMPLETED = "complete" # 処理が完了し、結果が保存された状態 18 | OCR_COMPLETED = "ocr_complete" # OCR処理が完了した状態 19 | ACCOUNT_SUGGESTED = "account_suggested" # 会計処理の提案が完了した状態 20 | ERROR = "error" # エラーが発生した状態 21 | 22 | 23 | class DisplayMode(str, Enum): 24 | """表示モード定義(UI)""" 25 | 26 | INPUT = "input" # 領収書入力モード 27 | HISTORY = "history" # 履歴表示モード 28 | 29 | 30 | class EventType(str, Enum): 31 | """イベントタイプ定義(ワークフロー)""" 32 | 33 | OCR_DONE = "ocr_done" 34 | ACCOUNT_SUGGESTED = "account_suggested" 35 | SAVE_COMPLETED = "save_completed" 36 | ERROR = "error" 37 | 38 | 39 | class CommandType(str, Enum): 40 | """コマンドタイプ定義(ワークフロー)""" 41 | 42 | APPROVE = "approve" 43 | REGENERATE = "regenerate" 44 | 45 | 46 | class Feedback(BaseModel): 47 | """フィードバックデータ(UI)""" 48 | 49 | command: CommandType 50 | content: str 51 | 52 | 53 | class ReceiptItem(BaseModel): 54 | """領収書の購入品目を表すモデル""" 55 | 56 | name: str = Field(description="商品名や項目名") 57 | price: str = Field(description="金額(数字のみ)") 58 | 59 | 60 | class ReceiptInfoItem(BaseModel): 61 | """領収書のその他情報を表すモデル""" 62 | 63 | key: str = Field(description="情報の種類(例: 支払方法、伝票番号など)") 64 | value: str = Field(description="情報の値") 65 | 66 | 67 | class ReceiptOCRResult(BaseModel): 68 | """OCR結果の構造化データモデル""" 69 | 70 | raw_text: str = Field(description="領収書から抽出された生テキスト") 71 | date: str = Field( 72 | description="領収書の日付(YYYY-MM-DD形式、不明な場合は空文字列)" 73 | ) 74 | amount: str = Field( 75 | description="金額(数字のみ、カンマなし、不明な場合は空文字列)" 76 | ) 77 | shop_name: str = Field(description="店舗・発行元名称(不明な場合は空文字列)") 78 | items: List[ReceiptItem] = Field( 79 | description="購入品目のリスト(ある場合のみ)", default_factory=list 80 | ) 81 | other_info: List[ReceiptInfoItem] = Field( 82 | description="その他抽出できた情報(領収書番号、支払方法など)", 83 | default_factory=list, 84 | ) 85 | 86 | 87 | class AccountInfo(BaseModel): 88 | """勘定科目情報のデータモデル(CSVにも保存される)""" 89 | 90 | date: str = Field(description="日付(YYYY-MM-DD形式)") 91 | account: str = Field(description="勘定科目") 92 | sub_account: str = Field(description="補助科目", default="") 93 | amount: str = Field(description="金額", default="") 94 | tax_amount: str = Field(description="消費税額", default="") 95 | vendor: str = Field(description="取引先", default="") 96 | invoice_number: str = Field( 97 | description="インボイス番号(通常Tから始まる文字列)", default="" 98 | ) 99 | description: str = Field(description="摘要", default="") 100 | reason: str = Field(description="この勘定科目と判断した理由") 101 | -------------------------------------------------------------------------------- /22/src/receipt_processor/storage.py: -------------------------------------------------------------------------------- 1 | """ 2 | CSV保存機能 3 | """ 4 | 5 | import csv 6 | import os 7 | from pathlib import Path 8 | from typing import Any, Dict, List 9 | 10 | import pandas as pd 11 | 12 | from src.receipt_processor.constants import CSV_FILE_PATH 13 | from src.receipt_processor.models import AccountInfo 14 | 15 | 16 | def save_to_csv(data: AccountInfo, csv_path: str = CSV_FILE_PATH) -> bool: 17 | """ 18 | データをCSVファイルに保存する 19 | 20 | Parameters: 21 | ----------- 22 | data: AccountInfo 23 | 保存するデータ(日付、金額、勘定科目情報など) 24 | csv_path: str 25 | CSVファイルのパス 26 | 27 | Returns: 28 | -------- 29 | bool 30 | 保存が成功したかどうか 31 | """ 32 | # 保存先ディレクトリが存在しない場合は作成 33 | csv_dir = os.path.dirname(csv_path) 34 | if not os.path.exists(csv_dir): 35 | os.makedirs(csv_dir) 36 | 37 | # ファイルが存在するかチェック 38 | file_exists = os.path.isfile(csv_path) 39 | 40 | # CSVに保存(新規作成または追記) 41 | try: 42 | with open(csv_path, mode="a", newline="", encoding="utf-8") as file: 43 | # AccountInfoのフィールド名を動的に取得 44 | fieldnames = list(AccountInfo.model_fields.keys()) 45 | writer = csv.DictWriter(file, fieldnames=fieldnames) 46 | 47 | # ファイルが存在しない場合はヘッダーを書き込む 48 | if not file_exists: 49 | writer.writeheader() 50 | 51 | # データを書き込む 52 | writer.writerow(data.model_dump()) 53 | 54 | return True 55 | except Exception as e: 56 | print(f"CSV保存エラー: {e}") 57 | return False 58 | 59 | 60 | def get_saved_receipts(csv_path: str = CSV_FILE_PATH) -> List[Dict[str, Any]]: 61 | """ 62 | 保存された領収書データを取得する 63 | 64 | Parameters: 65 | ----------- 66 | csv_path: str 67 | CSVファイルのパス 68 | 69 | Returns: 70 | -------- 71 | List[Dict[str, Any]] 72 | 保存されたデータのリスト 73 | """ 74 | if not os.path.exists(csv_path): 75 | return [] 76 | 77 | try: 78 | # pandasでCSVを読み込む 79 | df = pd.read_csv(csv_path, encoding="utf-8") 80 | 81 | # 古いフォーマットとの互換性のため、raw_textカラムが存在する場合は削除 82 | if "raw_text" in df.columns: 83 | df = df.drop(columns=["raw_text"]) 84 | 85 | # DataFrame -> Dict変換 86 | receipts = df.to_dict(orient="records") 87 | return receipts 88 | except Exception as e: 89 | print(f"CSV読み込みエラー: {e}") 90 | return [] 91 | 92 | 93 | def backup_csv(csv_path: str = CSV_FILE_PATH) -> bool: 94 | """ 95 | CSVファイルのバックアップを作成する 96 | 97 | Parameters: 98 | ----------- 99 | csv_path: str 100 | バックアップするCSVファイルのパス 101 | 102 | Returns: 103 | -------- 104 | bool 105 | バックアップが成功したかどうか 106 | """ 107 | if not os.path.exists(csv_path): 108 | return False 109 | 110 | try: 111 | # 元のファイルパスと拡張子を取得 112 | path = Path(csv_path) 113 | backup_path = path.with_name(f"{path.stem}_backup{path.suffix}") 114 | 115 | # ファイルをコピー 116 | import shutil 117 | 118 | shutil.copy2(csv_path, backup_path) 119 | return True 120 | except Exception as e: 121 | print(f"バックアップエラー: {e}") 122 | return False 123 | -------------------------------------------------------------------------------- /22/src/receipt_processor/ui_components.py: -------------------------------------------------------------------------------- 1 | """ 2 | Streamlit UI関連コンポーネント 3 | """ 4 | 5 | import tempfile 6 | import time 7 | from typing import Optional 8 | 9 | import pandas as pd 10 | import streamlit as st 11 | 12 | from src.receipt_processor.models import AccountInfo, CommandType, Feedback 13 | 14 | 15 | def setup_page() -> None: 16 | """ページの基本設定""" 17 | st.set_page_config( 18 | page_title="領収書OCRエージェント", 19 | page_icon="🧾", 20 | layout="wide", 21 | initial_sidebar_state="expanded", 22 | ) 23 | st.title("領収書OCRエージェント") 24 | 25 | 26 | def handle_image_input() -> Optional[str]: 27 | """ 28 | Streamlitでの画像入力処理(アップロードのみ) 29 | 30 | Returns: 31 | -------- 32 | Optional[str] 33 | 一時保存した画像ファイルのパス。画像がアップロードされていない場合はNone 34 | """ 35 | st.subheader("領収書画像を選択") 36 | 37 | # ファイルアップロード機能 38 | uploaded_file = st.file_uploader( 39 | "領収書画像をアップロード", type=["jpg", "jpeg", "png"] 40 | ) 41 | if uploaded_file is not None: 42 | # 画像プレビュー表示 43 | st.image(uploaded_file, caption="アップロード画像", use_container_width=True) 44 | 45 | # 処理用の一時ファイルに保存 46 | with tempfile.NamedTemporaryFile(delete=False, suffix=".jpg") as tmp: 47 | tmp.write(uploaded_file.getvalue()) 48 | image_path = tmp.name 49 | return image_path 50 | 51 | return None 52 | 53 | 54 | def display_ocr_text(ocr_text: str) -> None: 55 | """OCRテキストを表示""" 56 | with st.expander("OCR抽出テキスト", expanded=False): 57 | st.text_area("抽出されたテキスト", ocr_text, height=200, disabled=True) 58 | 59 | 60 | def account_info_editor(account_info: AccountInfo) -> None: 61 | """ 62 | 勘定科目情報の表示 63 | 64 | Parameters: 65 | ----------- 66 | account_info: Dict[str, Any] 67 | 表示する勘定科目情報 68 | 69 | Returns: 70 | -------- 71 | Dict[str, Any] 72 | 元の勘定科目情報 73 | """ 74 | # 生成理由 75 | st.markdown(account_info.reason) 76 | 77 | st.subheader("勘定科目情報") 78 | 79 | # 金額関連の情報(2列レイアウト) 80 | col1, col2 = st.columns(2) 81 | 82 | with col1: 83 | # 勘定科目 84 | st.text_input( 85 | "勘定科目", 86 | value=account_info.account, 87 | key="account_input", 88 | disabled=True, 89 | ) 90 | 91 | # 取引先 92 | st.text_input( 93 | "取引先", 94 | value=account_info.vendor, 95 | key="vendor_input", 96 | disabled=True, 97 | ) 98 | 99 | # 金額 100 | st.text_input( 101 | "金額", 102 | value=account_info.amount, 103 | key="amount_input", 104 | disabled=True, 105 | ) 106 | 107 | with col2: 108 | # 補助科目 109 | st.text_input( 110 | "補助科目", 111 | value=account_info.sub_account, 112 | key="sub_account_input", 113 | disabled=True, 114 | ) 115 | 116 | # インボイス番号 117 | st.text_input( 118 | "インボイス番号", 119 | value=account_info.invoice_number, 120 | key="invoice_number_input", 121 | disabled=True, 122 | ) 123 | 124 | # 消費税額 125 | st.text_input( 126 | "消費税額", 127 | value=account_info.tax_amount, 128 | key="tax_amount_input", 129 | disabled=True, 130 | ) 131 | 132 | # 摘要 133 | st.text_area( 134 | "摘要", 135 | value=account_info.description, 136 | key="description_input", 137 | disabled=True, 138 | ) 139 | 140 | 141 | def display_action_buttons() -> Optional[Feedback]: 142 | """ 143 | アクションボタンを表示 144 | 145 | Returns: 146 | -------- 147 | Optional[Feedback] 148 | フィードバック。ボタンが押されなかった場合はNone 149 | """ 150 | st.subheader("アクション") 151 | 152 | # 承認ボタン 153 | if st.button( 154 | "この情報で承認する", 155 | type="primary", 156 | use_container_width=True, 157 | ): 158 | feedback = Feedback( 159 | command=CommandType.APPROVE, 160 | content="", 161 | ) 162 | return feedback 163 | 164 | # 罫線(区切り線) 165 | st.markdown("---") 166 | 167 | # 単純なform外テキストエリアとボタンの組み合わせ 168 | feedback_text = st.text_area( 169 | "フィードバック内容", 170 | placeholder="例:「補助科目を交通費から駐車場代に変更して」「取引先名をカタカナで表記して」「摘要に日付を含めて」など", 171 | key="direct_feedback", 172 | height=100, 173 | ) 174 | 175 | # 説明文 176 | st.markdown( 177 | "自然言語でエージェントにフィードバックを送り、勘定科目情報を再生成できます" 178 | ) 179 | 180 | # 送信ボタン 181 | if st.button( 182 | "フィードバックを送信して再生成", 183 | type="secondary", 184 | use_container_width=True, 185 | key="send_feedback", 186 | ): 187 | if feedback_text: 188 | feedback = Feedback( 189 | command=CommandType.REGENERATE, 190 | content=feedback_text, 191 | ) 192 | return feedback 193 | 194 | return None 195 | 196 | 197 | def display_receipt_history(receipts: list) -> None: 198 | """ 199 | 保存された領収書履歴を表示 200 | 201 | Parameters: 202 | ----------- 203 | receipts: list 204 | 領収書データのリスト 205 | """ 206 | if not receipts: 207 | st.info("登録された領収書はありません") 208 | return 209 | 210 | st.subheader("登録済み領収書一覧") 211 | 212 | # DataFrameに変換して表示 213 | df = pd.DataFrame(receipts) 214 | 215 | # 生テキストは表示から除外 216 | if "raw_text" in df.columns: 217 | df = df.drop(columns=["raw_text"]) 218 | 219 | # データフレームを表示 220 | st.dataframe(df, use_container_width=True) 221 | 222 | # CSVダウンロード機能 223 | csv = df.to_csv(index=False).encode("utf-8") 224 | st.download_button( 225 | label="CSVダウンロード", 226 | data=csv, 227 | file_name="receipts_data.csv", 228 | mime="text/csv", 229 | ) 230 | 231 | 232 | def display_success_message() -> None: 233 | """保存成功メッセージを表示""" 234 | st.success("領収書データが正常に保存されました!", icon="✅") 235 | 236 | # 次のアクションを促すメッセージ 237 | st.subheader("次のアクション") 238 | 239 | # 新しい領収書を処理するボタン 240 | if st.button( 241 | "新しい領収書を登録する", 242 | type="primary", 243 | use_container_width=True, 244 | key="new_receipt_btn", 245 | ): 246 | # セッション状態をリセット 247 | st.session_state.workflow_state = "idle" 248 | st.session_state.ocr_text = "" 249 | st.session_state.ocr_result = {} 250 | st.session_state.account_info = {} 251 | # 画面を再読み込み 252 | st.rerun() 253 | 254 | 255 | def display_loading_spinner(message: str = "処理中...") -> None: 256 | """ローディングスピナーを表示""" 257 | with st.spinner(message): 258 | time.sleep(0.1) # スピナー表示のための最小遅延 259 | # 実際の処理は呼び出し側で行うため、ここでは何もしない 260 | -------------------------------------------------------------------------------- /22/src/receipt_processor/vision.py: -------------------------------------------------------------------------------- 1 | """ 2 | 画像処理とOCR機能 3 | """ 4 | 5 | import base64 6 | import mimetypes 7 | import os 8 | import pathlib 9 | import tempfile 10 | from typing import Any, Dict, List 11 | 12 | from langchain_anthropic import ChatAnthropic 13 | from PIL import Image, ImageEnhance 14 | 15 | from src.receipt_processor.constants import CLAUDE_FAST_MODEL 16 | from src.receipt_processor.models import ReceiptOCRResult 17 | 18 | 19 | def build_vision_message(image_path: str) -> List[Dict[str, Any]]: 20 | """ 21 | 画像をClaudeのVision APIで使用可能なメッセージ形式に変換 22 | 23 | Parameters: 24 | ----------- 25 | image_path: str 26 | 画像ファイルのパス 27 | 28 | Returns: 29 | -------- 30 | List[Dict[str, Any]] 31 | Claudeに送信するメッセージリスト 32 | """ 33 | path = pathlib.Path(image_path) 34 | data = path.read_bytes() 35 | media_type = mimetypes.guess_type(path.name)[0] or "image/png" 36 | b64 = base64.b64encode(data).decode() 37 | 38 | # OCRタスク用システムプロンプト 39 | system_prompt = """\ 40 | あなたは領収書OCRシステムです。画像内の領収書からテキストや情報を抽出し、指定された形式で返します。 41 | 以下の点に注意してください: 42 | 1. 日本語の領収書に特化してください 43 | 2. 日付、金額、店舗名は可能な限り抽出してください 44 | 3. 日付はYYYY-MM-DD形式に標準化してください 45 | 4. 金額は数字のみ(カンマなし)で抽出してください 46 | 5. 項目名と金額が対になっている場合は個別の品目として抽出してください 47 | 6. その他の重要情報(支払方法や領収書番号など)は種類と値のペアとして抽出してください 48 | 7. 生テキストは領収書の全テキストを含めてください 49 | """.strip() 50 | 51 | return [ 52 | { 53 | "role": "system", 54 | "content": system_prompt, 55 | }, 56 | { 57 | "role": "user", 58 | "content": [ 59 | { 60 | "type": "text", 61 | "text": "この画像から全てのテキストを抽出してください。", 62 | }, 63 | { 64 | "type": "image", 65 | "source": { 66 | "type": "base64", 67 | "media_type": media_type, 68 | "data": b64, 69 | }, 70 | }, 71 | ], 72 | }, 73 | ] 74 | 75 | 76 | def preprocess_receipt_image(image_path: str) -> str: 77 | """ 78 | OCR精度向上のための画像前処理 79 | 80 | Parameters: 81 | ----------- 82 | image_path: str 83 | 元の画像パス 84 | 85 | Returns: 86 | -------- 87 | str 88 | 処理後の画像の一時ファイルパス 89 | """ 90 | # 画像を開く 91 | img = Image.open(image_path) 92 | 93 | # グレースケール変換 94 | img_gray = img.convert("L") 95 | 96 | # コントラスト強調 97 | enhancer = ImageEnhance.Contrast(img_gray) 98 | img_enhanced = enhancer.enhance(2.0) # コントラスト2倍 99 | 100 | # 必要に応じてリサイズ(長辺が1000px以内に) 101 | max_size = 1000 102 | if max(img_enhanced.size) > max_size: 103 | ratio = max_size / max(img_enhanced.size) 104 | new_size = ( 105 | int(img_enhanced.size[0] * ratio), 106 | int(img_enhanced.size[1] * ratio), 107 | ) 108 | img_resized = img_enhanced.resize( 109 | new_size, 110 | Image.BICUBIC if hasattr(Image, "BICUBIC") else Image.Resampling.BICUBIC, 111 | ) 112 | else: 113 | img_resized = img_enhanced 114 | 115 | # 処理済み画像を一時ファイルに保存 116 | with tempfile.NamedTemporaryFile(delete=False, suffix=".jpg") as tmp: 117 | temp_path = tmp.name 118 | img_resized.save(temp_path, format="JPEG", quality=95) 119 | 120 | return temp_path 121 | 122 | 123 | def ocr_receipt( 124 | image_path: str, model_name: str = CLAUDE_FAST_MODEL 125 | ) -> ReceiptOCRResult: 126 | """ 127 | Claude Vision APIを使用して領収書画像からテキストを抽出し、構造化データとして返す 128 | 129 | Parameters: 130 | ----------- 131 | image_path: str 132 | 処理する画像のファイルパス 133 | model_name: str 134 | 使用するClaudeモデル名 135 | 136 | Returns: 137 | -------- 138 | ReceiptOCRResult 139 | 抽出された領収書情報(構造化データ) 140 | """ 141 | # 前処理を実行 142 | processed_image_path = preprocess_receipt_image(image_path) 143 | 144 | try: 145 | # LLMの初期化 146 | llm = ChatAnthropic( 147 | model_name=model_name, 148 | temperature=0, 149 | timeout=None, 150 | stop=None, 151 | max_retries=3, 152 | ) 153 | 154 | # 構造化出力を使って処理 155 | ocr_chain = llm.with_structured_output(ReceiptOCRResult) 156 | 157 | # OCR処理を実行 158 | result: ReceiptOCRResult = ocr_chain.invoke( 159 | build_vision_message(processed_image_path) 160 | ) # type: ignore 161 | 162 | return result 163 | finally: 164 | # 一時ファイルを削除 165 | if os.path.exists(processed_image_path): 166 | os.unlink(processed_image_path) 167 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 「実践LLMアプリケーション開発」サンプルコード 2 | 3 | このリポジトリは技術評論社Software Design誌の連載「実践LLMアプリケーション開発」に関連するサンプルコードを保存しています。 4 | 5 | ## 注意事項 6 | 7 | - このリポジトリのコードは自由に閲覧・二次利用が可能です。 8 | - ただし、すべて自己責任での利用となります。開発者や連載記事の著者は、いかなる責任も負いません。 9 | 10 | ソースコードに関する具体的な説明や解説については、連載記事の内容をご参照ください。 --------------------------------------------------------------------------------