diff --git a/Makefile b/Makefile index 860d660..b1ba115 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ setup: docker compose down - docker compose up -d + docker compose up -d --build enter_container: docker exec -it python_app bash diff --git a/src/__init__.py b/src/__init__.py index 59723e2..1aec526 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1,9 +1,4 @@ - -import logging - # import sentry_sdk -from common import settings -from sentry_sdk.integrations.logging import LoggingIntegration # sentry_logging = LoggingIntegration(level=logging.INFO, event_level=logging.ERROR) @@ -13,4 +8,4 @@ # integrations=[sentry_logging], # environment=settings.ENV, # traces_sample_rate=1.0, -# ) \ No newline at end of file +# ) diff --git a/src/common/__init__.py b/src/common/__init__.py index 16ee946..33fdb66 100644 --- a/src/common/__init__.py +++ b/src/common/__init__.py @@ -9,7 +9,5 @@ settings = read_yaml(os.path.join(base_dir, f"src/common/yaml_configs/{env}.yaml")) -init_logger( - os.path.join(settings.BASE_DIR, "src/common/logger/logging_config.yaml") -) +init_logger(os.path.join(settings.BASE_DIR, "src/common/logger/logging_config.yaml")) app_logger = logging.getLogger(__name__) diff --git a/src/common/config.py b/src/common/config.py index b1c21b7..a318d01 100644 --- a/src/common/config.py +++ b/src/common/config.py @@ -1,5 +1,3 @@ -import os - import yaml from pydantic import HttpUrl from pydantic_settings import BaseSettings diff --git a/src/common/yaml_configs/local.yaml b/src/common/yaml_configs/local.yaml index 756fbad..e1c1d1f 100644 --- a/src/common/yaml_configs/local.yaml +++ b/src/common/yaml_configs/local.yaml @@ -1,3 +1,3 @@ ENV: "local" SENTRY_DSN: -OPENAI_API_KEY: your-api-key \ No newline at end of file +OPENAI_API_KEY: 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 \ No newline at end of file diff --git a/src/main.py b/src/main.py index 5e4941b..072eb48 100644 --- a/src/main.py +++ b/src/main.py @@ -1,32 +1,36 @@ +import urllib.parse +import uuid +from typing import Any + import streamlit as st import streamlit_mermaid as stmd -import uuid -from src.model.models import EnglishWord -from src.common import app_logger +from pydantic import ValidationError +from service import OpenAISSEClient, WordQuestionBuilder -from service import WordQuestionBuilder, OpenAISSEClient +from src.common import app_logger +from src.model.models import EnglishWord USER_NAME = "user" ASSISTANT_NAME = "assistant" -def init_page(): +def init_page() -> None: st.set_page_config( - page_title="Utilizer: Boost your English active vocabulary 📚", - page_icon="📚" + page_title="UtiAIzer: Boost your English active vocabulary 📚", page_icon="📚" ) - st.header("Utilizer: Boost your English active vocabulary") + st.header("UtiAIzer: Boost your English active vocabulary") + st.write(":orange[Generated by chatgpt. No responsibility for any content.]") - -def generate_session_id(): +def generate_session_id() -> str: return str(uuid.uuid4()) -def convart_to_url_part(sentence): - return sentence.replace(" ", "%20") +def convart_to_url_part(sentence: str) -> str: + return urllib.parse.quote(sentence) + -def convart_to_under_score(sentence): - converted = sentence.replace(" ", "_") +def convart_to_under_score(sentence: str) -> str: + converted = sentence.replace(" ", "_") # del the fisrt, last char if it is "_" if converted[0] == "_": converted = converted[1:] @@ -34,38 +38,51 @@ def convart_to_under_score(sentence): converted = converted[:-1] return converted -def build_elsa_link(user_msg): - return f"\n - [Go To ELSA:{user_msg}](https://elsaspeak.com/en/learn-english/how-to-pronounce/{convart_to_url_part(user_msg)})" +def build_elsa_link(user_msg: str) -> str: + return f"\n - [Go To ELSA:{user_msg}](https://elsaspeak.com/en/learn-english/how-to-pronounce/{convart_to_url_part(user_msg)})" -def build_youglish_link(user_msg): - return f"\n\n - [Go To Youglish:{user_msg}](https://youglish.com/pronounce/{convart_to_url_part(user_msg)}/english?)" +def build_youglish_link(user_msg: str) -> str: + return f"\n\n - [Go To Youglish:{user_msg}](https://youglish.com/pronounce/{convart_to_url_part(user_msg)}/english?)" -def build_mermaid_graph_str(user_msg, result_str): +def build_mermaid_graph_str(user_msg: str, result_str: str) -> str: + app_logger.info(f"result_str: {result_str}") li = map(convart_to_under_score, result_str.split("|")) - # skip the first element + # skip the first and second + next(li) next(li) code = f""" graph TD - {user_msg} --> {next(li)} - {user_msg} --> {next(li)} - {user_msg} --> {next(li)} - {user_msg} --> {next(li)} + {convart_to_under_score(user_msg)} --> {next(li)} + {convart_to_under_score(user_msg)} --> {next(li)} + {convart_to_under_score(user_msg)} --> {next(li)} + {convart_to_under_score(user_msg)} --> {next(li)} """ return code -def ask_llm_sse(key, user_msg, assistant_msg, message_placeholder, is_collocation=False): +def ask_llm_sse( + key: str, + user_msg: str, + assistant_msg: str, + message_placeholder: Any, + is_collocation: bool = False, +) -> tuple[str, str]: question = WordQuestionBuilder.call(key=key, word=EnglishWord(value=user_msg)) answers = OpenAISSEClient.call(question) result_str = "" + count = 0 for answer in answers: + if count == 60: + assistant_msg += "\n\n :red[Too many answers.] \n\n" + message_placeholder.write(assistant_msg) + break - if answer.value == '[END]': + if answer.value == "[END]": message_placeholder.write(assistant_msg) break @@ -74,55 +91,107 @@ def ask_llm_sse(key, user_msg, assistant_msg, message_placeholder, is_collocatio message_placeholder.write(assistant_msg + "▌") result_str += answer.value + count += 1 return assistant_msg, result_str -def ask_several_questions_in_order(user_msg, assistant_msg, message_placeholder): +def ask_several_questions_in_order( + user_msg: str, assistant_msg: str, message_placeholder: Any +) -> str: # Meaning - assistant_msg += f"\n\n #### Meaning \n\n" - assistant_msg, _ = ask_llm_sse(key="meaning", user_msg=user_msg, assistant_msg=assistant_msg, - message_placeholder=message_placeholder) + assistant_msg += "\n\n #### Meaning \n\n" + assistant_msg, _ = ask_llm_sse( + key="meaning", + user_msg=user_msg, + assistant_msg=assistant_msg, + message_placeholder=message_placeholder, + ) # Pronunciation - assistant_msg += f"\n\n #### Pronunciation \n\n" + assistant_msg += "\n\n #### Pronunciation \n\n" assistant_msg += "\n - IPA: " - assistant_msg, _ = ask_llm_sse(key="pronunciation", user_msg=user_msg, assistant_msg=assistant_msg, - message_placeholder=message_placeholder) + assistant_msg, _ = ask_llm_sse( + key="pronunciation", + user_msg=user_msg, + assistant_msg=assistant_msg, + message_placeholder=message_placeholder, + ) # pronunciation_tip assistant_msg += "\n - Pronunciation Tip: " - assistant_msg, _ = ask_llm_sse(key="pronunciation_tip", user_msg=user_msg, assistant_msg=assistant_msg, - message_placeholder=message_placeholder) + assistant_msg, _ = ask_llm_sse( + key="pronunciation_tip", + user_msg=user_msg, + assistant_msg=assistant_msg, + message_placeholder=message_placeholder, + ) # st.write(build_youglish_link(user_msg) assistant_msg += build_youglish_link(user_msg) assistant_msg += build_elsa_link(user_msg) message_placeholder.write(assistant_msg) - # Origin - assistant_msg += f"\n\n #### Origin \n\n" - assistant_msg, _ = ask_llm_sse(key="origin", user_msg=user_msg, assistant_msg=assistant_msg, - message_placeholder=message_placeholder) + # # Origin + # assistant_msg += f"\n\n #### Origin \n\n" + # assistant_msg, _ = ask_llm_sse(key="origin", user_msg=user_msg, assistant_msg=assistant_msg, + # message_placeholder=message_placeholder) # Synonym - assistant_msg += f"\n\n #### Synonym \n\n" - assistant_msg, _ = ask_llm_sse(key="synonym", user_msg=user_msg, assistant_msg=assistant_msg, - message_placeholder=message_placeholder) + assistant_msg += "\n\n #### Synonym \n\n" + assistant_msg, _ = ask_llm_sse( + key="synonym", + user_msg=user_msg, + assistant_msg=assistant_msg, + message_placeholder=message_placeholder, + ) + + # Synonym + assistant_msg += "\n\n #### Antonym \n\n" + assistant_msg, _ = ask_llm_sse( + key="antonym", + user_msg=user_msg, + assistant_msg=assistant_msg, + message_placeholder=message_placeholder, + ) + # Example Sentence - assistant_msg += f"\n\n #### Example Sentence \n\n" - assistant_msg, result_str = ask_llm_sse(key="example_sentence", user_msg=user_msg, assistant_msg=assistant_msg, - message_placeholder=message_placeholder) - assistant_msg += build_elsa_link(result_str) + assistant_msg += "\n\n #### Example Sentence \n\n" + assistant_msg, result_str = ask_llm_sse( + key="example_sentence", + user_msg=user_msg, + assistant_msg=assistant_msg, + message_placeholder=message_placeholder, + ) + + # if AI find the example sentence, then build the url + if "I'm sorry" not in result_str and "Sorry" not in result_str: + assistant_msg += build_elsa_link(result_str) + + # # making_sentence_tips + # assistant_msg += "\n\n #### Making Sentence Tips \n\n" + # assistant_msg, _ = ask_llm_sse( + # key="making_sentence_tips", + # user_msg=user_msg, + # assistant_msg=assistant_msg, + # message_placeholder=message_placeholder, + # ) # Collocation - assistant_msg += f"\n\n #### Collocation MindMap \n\n" - assistant_msg, result_str = ask_llm_sse(key="collocation", user_msg=user_msg, assistant_msg=assistant_msg, - message_placeholder=message_placeholder, is_collocation=True) + assistant_msg += "\n\n #### Collocation MindMap \n\n" + assistant_msg, result_str = ask_llm_sse( + key="collocation", + user_msg=user_msg, + assistant_msg=assistant_msg, + message_placeholder=message_placeholder, + is_collocation=True, + ) app_logger.info(f"result_str: {result_str}") code = build_mermaid_graph_str(user_msg, result_str) assistant_msg += code app_logger.info(f"code: {code}") stmd.st_mermaid(code) + return assistant_msg -def ask(): + +def ask() -> None: current_session = st.session_state.current_session chat_log = st.session_state.chat_sessions[current_session] # 現在のセッションのチャット履歴を表示 @@ -139,13 +208,23 @@ def ask(): with st.chat_message(ASSISTANT_NAME): message_placeholder = st.empty() assistant_msg = "" - assistant_msg = ask_several_questions_in_order(user_msg, assistant_msg, message_placeholder) + try: + assistant_msg = ask_several_questions_in_order( + user_msg, assistant_msg, message_placeholder + ) + except ValidationError: + st.error( + "Please enter english word or phrase that is 30 characters or less including spaces." + ) + except Exception as e: + app_logger.exception(e) + # st.error('Sorry, something went wrong.') # セッションにチャットログを追加 chat_log.append({"name": USER_NAME, "msg": user_msg}) chat_log.append({"name": ASSISTANT_NAME, "msg": assistant_msg}) -def main(): +def main() -> None: init_page() # セッション情報の初期化 @@ -158,7 +237,9 @@ def main(): # Sidebarの実装 session_list = list(st.session_state.chat_sessions.keys()) - selected_session = st.sidebar.selectbox("Select Chat Session", session_list, index=len(session_list) - 1) + selected_session = st.sidebar.selectbox( + "Select Chat Session", session_list, index=len(session_list) - 1 + ) st.session_state.current_session = selected_session if st.sidebar.button("New Chat"): @@ -169,5 +250,5 @@ def main(): ask() -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/src/model/models.py b/src/model/models.py index f6c86a2..57c145e 100644 --- a/src/model/models.py +++ b/src/model/models.py @@ -1,15 +1,19 @@ from pydantic import BaseModel, Field, validator +class NotEnglishError(ValueError): + pass + + class EnglishWord(BaseModel): - value: str = Field(max_length=20) + value: str = Field(max_length=30) @validator("value") - def is_english(cls, v): + def is_english(cls, v: str) -> str: try: - v.encode('ascii') + v.encode("ascii") except UnicodeEncodeError: - raise ValueError("English only") + raise NotEnglishError("English only") from None return v diff --git a/src/service/__init__.py b/src/service/__init__.py index 8472eb1..35bddd4 100644 --- a/src/service/__init__.py +++ b/src/service/__init__.py @@ -1,7 +1,7 @@ -from .question_builder import WordQuestionBuilder from .openai_sse_client import OpenAISSEClient +from .question_builder import WordQuestionBuilder __all__ = [ "WordQuestionBuilder", "OpenAISSEClient", -] \ No newline at end of file +] diff --git a/src/service/openai_sse_client.py b/src/service/openai_sse_client.py index 800db93..8cecfe8 100644 --- a/src/service/openai_sse_client.py +++ b/src/service/openai_sse_client.py @@ -1,17 +1,19 @@ +from collections.abc import Iterable + import openai -from src.model.models import Question, Answer -from typing import Iterable -from src.common import settings -from src.common import app_logger + +from src.common import app_logger, settings +from src.model.models import Answer, Question openai.api_key = settings.OPENAI_API_KEY + class OpenAISSEClient: @classmethod - def call(cls, questions): - yield from cls(questions).__ask_llm_stream() + def call(cls, question: Question) -> Iterable[Answer]: + yield from cls(question).__ask_llm_stream() - def __init__(self, question) -> None: + def __init__(self, question: Question) -> None: self.__question = question def __ask_llm_stream(self) -> Iterable[Answer]: @@ -20,22 +22,16 @@ def __ask_llm_stream(self) -> Iterable[Answer]: model="gpt-3.5-turbo", stream=True, # SSEを使うための設定 messages=[ - { - "role": "system", - "content": "answer based on the following question" - }, - { - "role": "user", - "content": f"{self.__question.value}" - } + {"role": "system", "content": "answer based on the following question"}, + {"role": "user", "content": f"{self.__question.value}"}, ], ) app_logger.info(f"user_content: {self.__question.value}") for item in response: try: - content = item['choices'][0]['delta']['content'] - except: + content = item["choices"][0]["delta"]["content"] + except Exception: content = "" # dict型で返すことでよしなに変換してくれる yield Answer(value=content) diff --git a/src/service/question_builder.py b/src/service/question_builder.py index 1d657f8..035d530 100644 --- a/src/service/question_builder.py +++ b/src/service/question_builder.py @@ -1,31 +1,33 @@ -from src.model.models import Question, EnglishWord +from src.model.models import EnglishWord, Question Key = str class QuestionBuilderBase: - templates = NotImplementedError + templates: dict[Key, str] = {} def __init__(self, key: str, word: EnglishWord) -> None: self._key = key self._word = word @classmethod - def call(cls, key: Key, word: EnglishWord): + def call(cls, key: Key, word: EnglishWord) -> Question: return cls(key=key, word=word).__make_question() - def __make_question(self): + def __make_question(self) -> Question: question_value = self.templates[self._key].format(word=self._word.value) return Question(value=question_value) + class WordQuestionBuilder(QuestionBuilderBase): templates = { - "meaning": """output only the succinct meaning of "{word}" for kids""", - "origin": """output word roots of "{word}" for kids shortly""", + "meaning": """output the meaning of "{word}" shortly within 80 characters""", + "origin": """output word derivation of "{word}" within 100 characters""", "pronunciation": """output only the IPA for "{word}" within 20 characters""", - "pronunciation_tip": """Output the succinct pronunciation tip of "{word}" not based on IPA without preamble .Capitalize the characters should be emphasized.""", - "example_sentence": """Using "{word}, output one example sentence". Other words are easy for kids.""", - "making_sentence_tips": """output simple tips for making sentences with "{word}" correctly in terms on nuance and feeling for non-native speaker. Within 110 characters""", - "synonym": """output "{word}"s one synonym .Then tell the difference between "{word}" and the other for kids within 100 characters""", - "collocation" : """output "{word}"s collocations 6 times, separated with "|" . example : "apple pie|apple crisp" """, + "pronunciation_tip": """Output the pronunciation tip of "{word}" not based on IPA without preamble .Capitalize the characters should be emphasized. e.g: OHN-lee""", # noqa + "example_sentence": """Using "{word}, output one example sentence within 70 characters".""", + "making_sentence_tips": """output tips for making sentences using "{word}" in terms on nuance and feeling for non-native speaker. Within 80 characters""", # noqa + "synonym": """output "{word}"s one synonym .Then tell the difference between "{word}" and the other within 100 characters""", # noqa + "antonym": """output "{word}"s one antonym .""", # noqa + "collocation": """output "{word}"s collocations 7 times following grammer, separated with "|" . e.g: "{word} noun|{word} verb" """, # noqa } diff --git a/src/test_main.py b/src/test_main.py deleted file mode 100644 index b5c9803..0000000 --- a/src/test_main.py +++ /dev/null @@ -1,10 +0,0 @@ -from service import WordQuestionBuilder, OpenAISSEClient -from model.models import EnglishWord - -question = WordQuestionBuilder.call(key="collocation", word=EnglishWord(value="fart")) -answers = OpenAISSEClient.call(question) -result_str = "" -for answer in answers: - result_str += answer.value - -breakpoint() \ No newline at end of file diff --git a/src/tmp/api.py b/src/tmp/api.py deleted file mode 100644 index e49c7e1..0000000 --- a/src/tmp/api.py +++ /dev/null @@ -1,101 +0,0 @@ -import asyncio -import json -from functools import reduce - -import app.settings as settings -import openai - -openai.api_key = settings.OPEN_AP_API_KEY - - -class ChatGPTClient: - @classmethod - def async_call(cls, questions): - result_dict = asyncio.run(cls(questions).__async_call()) - return result_dict - - def __init__(self, questions) -> None: - self.__questions = questions - - async def __async_call(self): - tasks = [] - for question in self.__questions: - task = asyncio.create_task(self.__ask_chatgpt_async(question)) - tasks.append(task) - - results = await asyncio.gather(*tasks) - result_dict = reduce(lambda x, y: {**x, **y}, results) - return result_dict - - @staticmethod - async def __ask_chatgpt_async(question): - response = openai.ChatCompletion.create( - model="gpt-3.5-turbo", - top_p=1, - temperature=0, - messages=[ - {"role": "user", "content": question.text}, - ], - ) - return {question.key: response.choices[0]["message"]["content"].strip()} - - -from dataclasses import dataclass - - -@dataclass -class Question: - key: str - text: str - - -class QuestionBuilder: - templates = NotImplementedError - - def __init__(self, **kwargs) -> None: - self.kwargs = kwargs - - @classmethod - def call(cls, **kwargs): - return cls(**kwargs).__call() - - def __call(self): - return [ - Question(key, self.__make_question_async(key)) - for key in self.templates.keys() - ] - - def __make_question_async(self, key): - return self.templates[key].format(**self.kwargs) - - -class WordQuestionBuilder(QuestionBuilder): - # WordQuestionは全てのこのワードが全部大事だしなー。 - - templates = { - "pronaunciation": """output only the IPA for "{word}" within 20 characters""", - "meaning_ja": """Output only the meaning for "{word}" within 20 characters.lang:ja""", - "meaning_en": """output only the succinct meaning of "{word}" for kids""", - "pronaunciation_tips": """Output the succinct pronunciation tip of "{word}" not based on IPA without preamble .Capitalize the characters should be emphasized.""", - "example_sentence": """Output one example sentence with "{word}". Other words are simple.""", - "making_sentence_tips": """output simple tips for making sentences with "{word}" correctly in terms on nuance and feeling for non-native speaker. Within 110 characters""", - "synonym": """output "{word}"s one synonym .Then tell the difference between "{word}" and the other for kids within 100 characters""", - } - - -class SentenceFeedBackBuilder(QuestionBuilder): - templates = { - "grammar_correction": """correct grammar for "{sentence}" within 100 characters""", - } - - -# Main program -def main(): - questions = WordQuestionBuilder.call(word="imprudent") - # questions = SentenceFeedBackBuilder.call(sentence="she no want me to buy the car.") - resp = ChatGPTCilent.async_call(questions) - print(resp) - - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/src/tmp/parse.py b/src/tmp/parse.py deleted file mode 100644 index a6c2540..0000000 --- a/src/tmp/parse.py +++ /dev/null @@ -1,14 +0,0 @@ -import re - -pronunciation = "uh-PRAY-zuhl" -special_chars = "–,-, ,_" - -# Remove special characters except for space -# 文字列内の特殊文字を', 'に置き換えるための変換テーブルを作成します -trans = str.maketrans({char: "," for char in special_chars}) - -# 文字列内の特殊文字を', 'に置き換えます -output_str = pronunciation.translate(trans) - -# Join the pronunciation list with ", " -print(output_str) \ No newline at end of file diff --git a/tests/module/test_temp_class.py b/tests/module/test_temp_class.py deleted file mode 100644 index 4f23b22..0000000 --- a/tests/module/test_temp_class.py +++ /dev/null @@ -1,8 +0,0 @@ -from src.service.temp_class import TempClass - - -class TestTempClass: - def test_add_temp(self) -> None: - got = TempClass().add_temp("test_input_str_") - - assert "test_input_str_temp" == got