diff --git a/pyproject.toml b/pyproject.toml index 95c392c..ac9c97d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,7 @@ dependencies = [ "elasticsearch>=9.1.0", "fastapi[standard]>=0.116.1", "foundry-local-sdk>=0.4.0", + "gtts>=2.5.4", "httpx>=0.28.1", "jinja2>=3.1.2", "langchain-azure-ai>=0.1.4", diff --git a/template_langgraph/services/streamlits/pages/chat_with_tools_agent.py b/template_langgraph/services/streamlits/pages/chat_with_tools_agent.py index e9562e7..e93b800 100644 --- a/template_langgraph/services/streamlits/pages/chat_with_tools_agent.py +++ b/template_langgraph/services/streamlits/pages/chat_with_tools_agent.py @@ -1,3 +1,5 @@ +from base64 import b64encode + import streamlit as st from langchain_community.callbacks.streamlit import ( StreamlitCallbackHandler, @@ -9,6 +11,11 @@ ) from template_langgraph.tools.common import get_default_tools + +def image_to_base64(image_bytes: bytes) -> str: + return b64encode(image_bytes).decode("utf-8") + + if "chat_history" not in st.session_state: st.session_state["chat_history"] = [] @@ -43,16 +50,89 @@ for msg in st.session_state["chat_history"]: if isinstance(msg, dict): - st.chat_message(msg["role"]).write(msg["content"]) + attachments = msg.get("attachments", []) + with st.chat_message(msg["role"]): + if attachments: + for item in attachments: + if item["type"] == "text": + st.markdown(item["text"]) + elif item["type"] == "image_url": + st.image(item["image_url"]["url"]) + else: + st.write(msg["content"]) else: st.chat_message("assistant").write(msg.content) -if prompt := st.chat_input(): - st.session_state["chat_history"].append({"role": "user", "content": prompt}) - st.chat_message("user").write(prompt) +if prompt := st.chat_input( + accept_file="multiple", + file_type=[ + "png", + "jpg", + "jpeg", + "gif", + "webp", + ], +): + user_display_items = [] + message_parts = [] + + prompt_text = prompt if isinstance(prompt, str) else getattr(prompt, "text", "") or "" + prompt_files = [] if isinstance(prompt, str) else (getattr(prompt, "files", []) or []) + + user_text = prompt_text + if user_text.strip(): + user_display_items.append({"type": "text", "text": user_text}) + message_parts.append(user_text) + + has_unsupported_files = False + for file in prompt_files: + if file.type and file.type.startswith("image/"): + image_bytes = file.getvalue() + base64_image = image_to_base64(image_bytes) + image_url = f"data:{file.type};base64,{base64_image}" + user_display_items.append( + { + "type": "image_url", + "image_url": {"url": image_url}, + } + ) + message_parts.append(f"![image]({image_url})") + else: + has_unsupported_files = True + + if has_unsupported_files: + st.warning("画像ファイル以外の添付は現在サポートされていません。") + + message_content = "\n\n".join(message_parts).strip() + if not message_content: + message_content = "ユーザーが画像をアップロードしました。" + + new_user_message = {"role": "user", "content": message_content} + if user_display_items: + new_user_message["attachments"] = user_display_items + + st.session_state["chat_history"].append(new_user_message) + + with st.chat_message("user"): + if user_display_items: + for item in user_display_items: + if item["type"] == "text": + st.markdown(item["text"]) + elif item["type"] == "image_url": + st.image(item["image_url"]["url"]) + else: + st.write(message_content) + + graph_messages = [] + for msg in st.session_state["chat_history"]: + if isinstance(msg, dict): + graph_messages.append({"role": msg["role"], "content": msg["content"]}) + else: + graph_messages.append(msg) + with st.chat_message("assistant"): response: AgentState = st.session_state["graph"].invoke( - {"messages": st.session_state["chat_history"]}, + {"messages": graph_messages}, { "callbacks": [ StreamlitCallbackHandler(st.container()), diff --git a/uv.lock b/uv.lock index 1c945d0..aa338d7 100644 --- a/uv.lock +++ b/uv.lock @@ -722,14 +722,14 @@ wheels = [ [[package]] name = "click" -version = "8.2.1" +version = "8.1.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, ] [[package]] @@ -2030,6 +2030,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/23/4f/f27c973ff50486a70be53a3978b6b0244398ca170a4e19d91988b5295d92/grpcio_tools-1.75.1-cp314-cp314-win_amd64.whl", hash = "sha256:878c3b362264588c45eba57ce088755f8b2b54893d41cc4a68cdeea62996da5c", size = 1189364, upload-time = "2025-09-26T09:09:42.036Z" }, ] +[[package]] +name = "gtts" +version = "2.5.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/79/5ddb1dfcd663581d0d3fca34ccb1d8d841b47c22a24dc8dce416e3d87dfa/gtts-2.5.4.tar.gz", hash = "sha256:f5737b585f6442f677dbe8773424fd50697c75bdf3e36443585e30a8d48c1884", size = 24018, upload-time = "2024-11-10T21:58:00.358Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/6c/8b8b1fdcaee7e268536f1bb00183a5894627726b54a9ddc6fc9909888447/gTTS-2.5.4-py3-none-any.whl", hash = "sha256:5dd579377f9f5546893bc26315ab1f846933dc27a054764b168f141065ca8436", size = 29184, upload-time = "2024-11-10T21:57:58.448Z" }, +] + [[package]] name = "gunicorn" version = "23.0.0" @@ -6380,6 +6393,7 @@ dependencies = [ { name = "elasticsearch" }, { name = "fastapi", extra = ["standard"] }, { name = "foundry-local-sdk" }, + { name = "gtts" }, { name = "httpx" }, { name = "jinja2" }, { name = "langchain-azure-ai" }, @@ -6435,6 +6449,7 @@ requires-dist = [ { name = "elasticsearch", specifier = ">=9.1.0" }, { name = "fastapi", extras = ["standard"], specifier = ">=0.116.1" }, { name = "foundry-local-sdk", specifier = ">=0.4.0" }, + { name = "gtts", specifier = ">=2.5.4" }, { name = "httpx", specifier = ">=0.28.1" }, { name = "jinja2", specifier = ">=3.1.2" }, { name = "langchain-azure-ai", specifier = ">=0.1.4" },