├── 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 | 
--------------------------------------------------------------------------------
/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 | ソースコードに関する具体的な説明や解説については、連載記事の内容をご参照ください。
--------------------------------------------------------------------------------