Skip to content

Commit b31b314

Browse files
authored
Merge pull request #134 from ks6088ts-labs/feature/issue-133_add-frontend
add frontend apps
2 parents 972b6c5 + 27e6860 commit b31b314

File tree

10 files changed

+507
-134
lines changed

10 files changed

+507
-134
lines changed

frontend/.env.sample

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,16 @@ BACKEND_URL = "http://localhost:8888"
55
AZURE_OPENAI_ENDPOINT = "https://<aoai-name>.openai.azure.com"
66
AZURE_OPENAI_API_KEY = "<aoai-api-key>"
77
AZURE_OPENAI_API_VERSION = "2024-05-01-preview"
8-
AZURE_OPENAI_WHISPER_MODEL = "whisper"
9-
AZURE_OPENAI_GPT_MODEL = "gpt-4o"
8+
AZURE_OPENAI_MODEL_WHISPER = "whisper"
9+
AZURE_OPENAI_MODEL_CHAT = "gpt-4o"
10+
AZURE_OPENAI_MODEL_EMBEDDING = "text-embedding-3-large"
11+
12+
# Azure AI Search
13+
AZURE_AI_SEARCH_ENDPOINT = "https://<your-aisearch-name>.search.windows.net"
14+
AZURE_AI_SEARCH_API_KEY = "<api-key>"
15+
16+
# LangSmith
17+
LANGCHAIN_TRACING_V2 = "false" # set to "true" to enable tracing
18+
LANGCHAIN_API_KEY = "<api-key>"
19+
LANGCHAIN_ENDPOINT = "https://api.smith.langchain.com"
20+
LANGCHAIN_PROJECT = "default"

frontend/pages/chat.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def main(
3737
st.markdown(prompt)
3838

3939
response = client.chat.completions.create(
40-
model=getenv("AZURE_OPENAI_GPT_MODEL"),
40+
model=getenv("AZURE_OPENAI_MODEL_CHAT"),
4141
messages=[{"role": m["role"], "content": m["content"]} for m in st.session_state.messages],
4242
stream=True,
4343
)
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import logging
2+
import traceback
3+
from os import getenv
4+
from urllib.parse import urlparse
5+
6+
import streamlit as st
7+
import tiktoken
8+
from dotenv import load_dotenv
9+
from langchain_community.document_loaders import YoutubeLoader # Youtube用
10+
from langchain_core.output_parsers import StrOutputParser
11+
from langchain_core.prompts import ChatPromptTemplate
12+
from langchain_core.runnables import RunnableLambda
13+
from langchain_openai import AzureChatOpenAI
14+
from langchain_text_splitters import RecursiveCharacterTextSplitter
15+
16+
logger = logging.getLogger(__name__)
17+
load_dotenv()
18+
19+
20+
SUMMARIZE_PROMPT = """Please provide a clear 300 word summary of the following content in Japanese.
21+
22+
========
23+
24+
{content}
25+
26+
========
27+
"""
28+
29+
30+
def init_page():
31+
st.set_page_config(page_title="Summarize YouTube", page_icon="💻")
32+
st.header("Summarize YouTube")
33+
st.sidebar.title("Options")
34+
35+
36+
def select_model(temperature=0):
37+
return AzureChatOpenAI(
38+
temperature=temperature,
39+
api_key=getenv("AZURE_OPENAI_API_KEY"),
40+
api_version=getenv("AZURE_OPENAI_API_VERSION"),
41+
azure_endpoint=getenv("AZURE_OPENAI_ENDPOINT"),
42+
model=getenv("AZURE_OPENAI_MODEL_CHAT"),
43+
)
44+
45+
46+
def init_summarize_chain():
47+
llm = select_model()
48+
prompt = ChatPromptTemplate.from_messages(
49+
[
50+
("user", SUMMARIZE_PROMPT),
51+
]
52+
)
53+
output_parser = StrOutputParser()
54+
return prompt | llm | output_parser
55+
56+
57+
def init_map_reduce_chain():
58+
summarize_chain = init_summarize_chain()
59+
60+
text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
61+
model_name="gpt-4o", # hard-coded for now
62+
chunk_size=16000,
63+
chunk_overlap=0,
64+
)
65+
text_split = RunnableLambda(lambda x: [{"content": doc} for doc in text_splitter.split_text(x["content"])])
66+
text_concat = RunnableLambda(lambda x: {"content": "\n".join(x)})
67+
return text_split | summarize_chain.map() | text_concat | summarize_chain
68+
69+
70+
def init_chain():
71+
summarize_chain = init_summarize_chain()
72+
map_reduce_chain = init_map_reduce_chain()
73+
74+
def route(x):
75+
encoding = tiktoken.encoding_for_model("gpt-4o")
76+
token_count = len(encoding.encode(x["content"]))
77+
if token_count > 16000:
78+
return map_reduce_chain
79+
else:
80+
return summarize_chain
81+
82+
chain = RunnableLambda(route)
83+
84+
return chain
85+
86+
87+
def validate_url(url):
88+
"""URLが有効かどうかを判定する関数"""
89+
try:
90+
result = urlparse(url)
91+
if result.netloc != "www.youtube.com":
92+
return False
93+
if not result.path.startswith("/watch"):
94+
return False
95+
return all([result.scheme, result.netloc])
96+
except ValueError:
97+
return False
98+
99+
100+
def get_content(url):
101+
with st.spinner("Fetching Youtube ..."):
102+
loader = YoutubeLoader.from_youtube_url(
103+
url,
104+
add_video_info=True, # タイトルや再生数も取得できる
105+
language=["en", "ja"], # 英語→日本語の優先順位で字幕を取得
106+
)
107+
res = loader.load() # list of `Document` (page_content, metadata)
108+
try:
109+
if res:
110+
content = res[0].page_content
111+
title = res[0].metadata["title"]
112+
return f"Title: {title}\n\n{content}"
113+
else:
114+
return None
115+
except Exception as e:
116+
logger.error(f"An error occurred: {e}")
117+
st.write(traceback.format_exc())
118+
return None
119+
120+
121+
def main():
122+
init_page()
123+
chain = init_chain()
124+
if url := st.text_input("URL: ", key="input"):
125+
# clear text input
126+
is_valid_url = validate_url(url)
127+
if not is_valid_url:
128+
st.write("Please input valid url")
129+
else:
130+
if content := get_content(url):
131+
st.markdown("## Summary")
132+
st.write_stream(chain.stream({"content": content}))
133+
st.markdown("---")
134+
st.markdown("## Original Text")
135+
st.write(content)
136+
137+
138+
if __name__ == "__main__":
139+
main()

frontend/pages/tool_agent.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import logging
2+
from os import getenv
3+
4+
import streamlit as st
5+
from dotenv import load_dotenv
6+
from langchain.agents import AgentExecutor, create_tool_calling_agent
7+
from langchain.memory import ConversationBufferWindowMemory
8+
from langchain_community.callbacks import StreamlitCallbackHandler
9+
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
10+
from langchain_core.runnables import RunnableConfig
11+
from langchain_openai import AzureChatOpenAI
12+
from tools.fetch_contoso_rules import fetch_contoso_rules
13+
from tools.search_ddg import search_ddg
14+
15+
logger = logging.getLogger(__name__)
16+
load_dotenv()
17+
18+
19+
CUSTOM_SYSTEM_PROMPT = """
20+
あなたは、ユーザーのリクエストに基づいてインターネットで調べ物を行うアシスタントです。
21+
利用可能なツールを使用して、調査した情報を説明してください。
22+
既に知っていることだけに基づいて答えないでください。回答する前にできる限り検索を行ってください。
23+
(ユーザーが読むページを指定するなど、特別な場合は、検索する必要はありません。)
24+
25+
検索結果ページを見ただけでは情報があまりないと思われる場合は、次の2つのオプションを検討して試してみてください。
26+
27+
- 検索結果のリンクをクリックして、各ページのコンテンツにアクセスし、読んでみてください。
28+
- 1ページが長すぎる場合は、3回以上ページ送りしないでください(メモリの負荷がかかるため)。
29+
- 検索クエリを変更して、新しい検索を実行してください。
30+
- 検索する内容に応じて検索に利用する言語を適切に変更してください。
31+
- 例えば、プログラミング関連の質問については英語で検索するのがいいでしょう。
32+
33+
ユーザーは非常に忙しく、あなたほど自由ではありません。
34+
そのため、ユーザーの労力を節約するために、直接的な回答を提供してください。
35+
36+
=== 悪い回答の例 ===
37+
- これらのページを参照してください。
38+
- これらのページを参照してコードを書くことができます。
39+
- 次のページが役立つでしょう。
40+
41+
=== 良い回答の例 ===
42+
- これはサンプルコードです。 -- サンプルコードをここに --
43+
- あなたの質問の答えは -- 回答をここに --
44+
45+
回答の最後には、参照したページのURLを**必ず**記載してください。(これにより、ユーザーは回答を検証することができます)
46+
47+
ユーザーが使用している言語で回答するようにしてください。
48+
ユーザーが日本語で質問した場合は、日本語で回答してください。ユーザーがスペイン語で質問した場合は、スペイン語で回答してください。
49+
"""
50+
51+
52+
def init_page():
53+
st.set_page_config(page_title="Web Browsing Agent", page_icon="🤗")
54+
st.header("Web Browsing Agent 🤗")
55+
st.sidebar.title("Options")
56+
57+
58+
def init_messages():
59+
clear_button = st.sidebar.button("Clear Conversation", key="clear")
60+
if clear_button or "messages" not in st.session_state:
61+
st.session_state.messages = [{"role": "assistant", "content": "Please ask me any questions you may have."}]
62+
st.session_state["memory"] = ConversationBufferWindowMemory(
63+
return_messages=True, memory_key="chat_history", k=10
64+
)
65+
66+
67+
def create_agent():
68+
tools = [
69+
search_ddg,
70+
fetch_contoso_rules,
71+
]
72+
prompt = ChatPromptTemplate.from_messages(
73+
[
74+
("system", CUSTOM_SYSTEM_PROMPT),
75+
MessagesPlaceholder(variable_name="chat_history"),
76+
("user", "{input}"),
77+
MessagesPlaceholder(variable_name="agent_scratchpad"),
78+
]
79+
)
80+
llm = AzureChatOpenAI(
81+
temperature=0,
82+
api_key=getenv("AZURE_OPENAI_API_KEY"),
83+
api_version=getenv("AZURE_OPENAI_API_VERSION"),
84+
azure_endpoint=getenv("AZURE_OPENAI_ENDPOINT"),
85+
model=getenv("AZURE_OPENAI_MODEL_CHAT"),
86+
)
87+
agent = create_tool_calling_agent(llm, tools, prompt)
88+
return AgentExecutor(
89+
agent=agent,
90+
tools=tools,
91+
verbose=True,
92+
memory=st.session_state["memory"],
93+
)
94+
95+
96+
def main():
97+
init_page()
98+
init_messages()
99+
web_browsing_agent = create_agent()
100+
101+
for msg in st.session_state["memory"].chat_memory.messages:
102+
st.chat_message(msg.type).write(msg.content)
103+
104+
if prompt := st.chat_input(placeholder="Type your question here..."):
105+
st.chat_message("user").write(prompt)
106+
107+
with st.chat_message("assistant"):
108+
st_cb = StreamlitCallbackHandler(st.container(), expand_new_thoughts=True)
109+
response = web_browsing_agent.invoke(
110+
{"input": prompt},
111+
config=RunnableConfig({"callbacks": [st_cb]}),
112+
)
113+
st.write(response["output"])
114+
115+
116+
if __name__ == "__main__":
117+
main()

frontend/pages/transcription.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def get_transcription(file_path: str) -> Transcription:
2121

2222
return client.audio.transcriptions.create(
2323
file=open(file=file_path, mode="rb"),
24-
model=getenv("AZURE_OPENAI_WHISPER_MODEL"),
24+
model=getenv("AZURE_OPENAI_MODEL_WHISPER"),
2525
)
2626

2727

frontend/tools/__init__.py

Whitespace-only changes.
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# GitHub: https://github.com/naotaka1128/llm_app_codes/chapter_010/tools/fetch_qa_content.py
2+
3+
from os import getenv
4+
5+
from langchain_community.vectorstores.azuresearch import AzureSearch
6+
from langchain_core.pydantic_v1 import BaseModel, Field
7+
from langchain_core.tools import tool
8+
from langchain_openai import AzureOpenAIEmbeddings
9+
10+
11+
class FetchContentInput(BaseModel):
12+
"""型を指定するためのクラス"""
13+
14+
query: str = Field()
15+
16+
17+
def get_embeddings():
18+
return AzureOpenAIEmbeddings(
19+
api_key=getenv("AZURE_OPENAI_API_KEY"),
20+
api_version=getenv("AZURE_OPENAI_API_VERSION"),
21+
azure_endpoint=getenv("AZURE_OPENAI_ENDPOINT"),
22+
azure_deployment=getenv("AZURE_OPENAI_MODEL_EMBEDDING"),
23+
)
24+
25+
26+
def create_azure_search(index_name: str) -> AzureSearch:
27+
return AzureSearch(
28+
azure_search_endpoint=getenv("AZURE_AI_SEARCH_ENDPOINT"),
29+
azure_search_key=getenv("AZURE_AI_SEARCH_API_KEY"),
30+
index_name=index_name,
31+
embedding_function=get_embeddings().embed_query,
32+
additional_search_client_options={"retry_total": 4},
33+
)
34+
35+
36+
@tool(args_schema=FetchContentInput)
37+
def fetch_contoso_rules(query):
38+
"""
39+
Contoso 社の就業規則情報から、関連するコンテンツを見つけるツールです。
40+
Contoso 社に関する具体的な知識を得るのに役立ちます。
41+
42+
このツールは `similarity`(類似度)と `content`(コンテンツ)を返します。
43+
- 'similarity'は、回答が質問にどの程度関連しているかを示します。
44+
値が高いほど、質問との関連性が高いことを意味します。
45+
'similarity'値が0.5未満のドキュメントは返されません。
46+
- 'content'は、質問に対する回答のテキストを提供します。
47+
通常、よくある質問とその対応する回答で構成されています。
48+
49+
空のリストが返された場合、ユーザーの質問に対する回答が見つからなかったことを意味します。
50+
その場合、ユーザーに質問内容を明確にしてもらうのが良いでしょう。
51+
52+
Returns
53+
-------
54+
List[Dict[str, Any]]:
55+
- page_content
56+
- similarity: float
57+
- content: str
58+
"""
59+
db = create_azure_search("contoso_rules")
60+
docs = db.similarity_search_with_relevance_scores(
61+
query=query,
62+
k=3,
63+
score_threshold=0.5,
64+
)
65+
return [
66+
{
67+
"similarity": similarity,
68+
"content": i.page_content,
69+
}
70+
for i, similarity in docs
71+
]
72+
73+
74+
if __name__ == "__main__":
75+
import logging
76+
77+
from dotenv import load_dotenv
78+
79+
logging.basicConfig(
80+
format="[%(asctime)s] %(levelname)7s from %(name)s in %(pathname)s:%(lineno)d: " "%(message)s",
81+
level=logging.DEBUG,
82+
force=True,
83+
)
84+
85+
load_dotenv()
86+
docs = fetch_contoso_rules("ドレスコード")
87+
for doc in docs:
88+
print(doc)

0 commit comments

Comments
 (0)