From 965bee818a89fb2c243fb6feea7f53b3e8d31c7f Mon Sep 17 00:00:00 2001 From: misyaguziya <53165965+misyaguziya@users.noreply.github.com> Date: Fri, 17 Oct 2025 15:58:50 +0900 Subject: [PATCH] =?UTF-8?q?LM=20Studio=20=E3=81=A8=20Ollama=20=E3=81=AE?= =?UTF-8?q?=E7=BF=BB=E8=A8=B3=E3=82=AF=E3=83=A9=E3=82=A4=E3=82=A2=E3=83=B3?= =?UTF-8?q?=E3=83=88=E3=81=A8=E3=83=97=E3=83=AD=E3=83=B3=E3=83=97=E3=83=88?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0=E3=80=81requirements=20=E3=81=AB=20?= =?UTF-8?q?langchain-ollama=20=E3=82=92=E8=BF=BD=E8=A8=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - src-python/models/translation に LM Studio 用 (translation_lmstudio.py / translation_lmstudio.yml) を追加 - Ollama 用クライアント (translation_ollama.py / translation_ollama.yml) を追加 - 各クライアントでプロンプト YAML から system_prompt / supported_languages を読み込み、認証チェック・モデル一覧取得・モデル設定・クライアント更新・translate 呼び出しを実装 - requirements.txt と requirements_cuda.txt に langchain-ollama==0.3.10 を追記 --- requirements.txt | 1 + requirements_cuda.txt | 1 + .../prompt/translation_lmstudio.yml | 48 +++++++ .../translation/prompt/translation_ollama.yml | 48 +++++++ .../translation/translation_lmstudio.py | 124 ++++++++++++++++++ .../models/translation/translation_ollama.py | 115 ++++++++++++++++ 6 files changed, 337 insertions(+) create mode 100644 src-python/models/translation/prompt/translation_lmstudio.yml create mode 100644 src-python/models/translation/prompt/translation_ollama.yml create mode 100644 src-python/models/translation/translation_lmstudio.py create mode 100644 src-python/models/translation/translation_ollama.py diff --git a/requirements.txt b/requirements.txt index 6cf0b473..3d0a6670 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,6 +22,7 @@ langchain-openai==0.3.32 langchain-google-genai==2.1.10 google-genai==1.45.0 grpcio==1.67.1 +langchain-ollama==0.3.10 SudachiPy==0.6.10 SudachiDict-core==20250825 SudachiDict-full==20250825 diff --git a/requirements_cuda.txt b/requirements_cuda.txt index b411a604..60f0e9e1 100644 --- a/requirements_cuda.txt +++ b/requirements_cuda.txt @@ -23,6 +23,7 @@ langchain-openai==0.3.32 langchain-google-genai==2.1.10 google-genai==1.45.0 grpcio==1.67.1 +langchain-ollama==0.3.10 SudachiPy==0.6.10 SudachiDict-core==20250825 SudachiDict-full==20250825 diff --git a/src-python/models/translation/prompt/translation_lmstudio.yml b/src-python/models/translation/prompt/translation_lmstudio.yml new file mode 100644 index 00000000..8c7b56fc --- /dev/null +++ b/src-python/models/translation/prompt/translation_lmstudio.yml @@ -0,0 +1,48 @@ +system_prompt: | + You are a helpful translation assistant. + Supported languages: + {supported_languages} + + Translate the user provided text from {input_lang} to {output_lang}. + Return ONLY the translated text. Do not add quotes or extra commentary. + +supported_languages: | + Arabic + Bengali + Bulgarian + Simplified Chinese + Traditional Chinese + Croatian + Czech + Danish + Dutch + English + Estonian + Finnish + French + German + Greek + Hebrew + Hindi + Hungarian + Indonesian + Italian + Japanese + Korean + Latvian + Lithuanian + Norwegian + Polish + Portuguese + Romanian + Russian + Serbian + Slovak + Slovenian + Spanish + Swahili + Swedish + Thai + Turkish + Ukrainian + Vietnamese diff --git a/src-python/models/translation/prompt/translation_ollama.yml b/src-python/models/translation/prompt/translation_ollama.yml new file mode 100644 index 00000000..8c7b56fc --- /dev/null +++ b/src-python/models/translation/prompt/translation_ollama.yml @@ -0,0 +1,48 @@ +system_prompt: | + You are a helpful translation assistant. + Supported languages: + {supported_languages} + + Translate the user provided text from {input_lang} to {output_lang}. + Return ONLY the translated text. Do not add quotes or extra commentary. + +supported_languages: | + Arabic + Bengali + Bulgarian + Simplified Chinese + Traditional Chinese + Croatian + Czech + Danish + Dutch + English + Estonian + Finnish + French + German + Greek + Hebrew + Hindi + Hungarian + Indonesian + Italian + Japanese + Korean + Latvian + Lithuanian + Norwegian + Polish + Portuguese + Romanian + Russian + Serbian + Slovak + Slovenian + Spanish + Swahili + Swedish + Thai + Turkish + Ukrainian + Vietnamese diff --git a/src-python/models/translation/translation_lmstudio.py b/src-python/models/translation/translation_lmstudio.py new file mode 100644 index 00000000..6de2f615 --- /dev/null +++ b/src-python/models/translation/translation_lmstudio.py @@ -0,0 +1,124 @@ +from openai import OpenAI +from langchain_openai import ChatOpenAI +from pydantic import SecretStr +import yaml +from os import path as os_path + +def _authentication_check(api_key: str, base_url: str | None = None) -> bool: + """Check if the provided API key is valid by attempting to list models. + """ + try: + client = OpenAI(api_key=api_key, base_url=base_url) + client.models.list() + return True + except Exception: + return False + +def _get_available_text_models(api_key: str, base_url: str | None = None) -> list[str]: + """Extract the list of available text models from the LM Studio. + """ + client = OpenAI(api_key=api_key, base_url=base_url) + res = client.models.list() + allowed_models = [] + + for model in res.data: + allowed_models.append(model.id) + + allowed_models.sort() + return allowed_models + +def _load_prompt_config(root_path: str = None) -> dict: + prompt_filename = "translation_lmstudio.yml" + # PyInstaller 展開後 + if root_path and os_path.exists(os_path.join(root_path, "_internal", "prompt", prompt_filename)): + prompt_path = os_path.join(root_path, "_internal", "prompt", prompt_filename) + # src-python 直下実行 + elif os_path.exists(os_path.join(os_path.dirname(__file__), "models", "translation", "prompt", prompt_filename)): + prompt_path = os_path.join(os_path.dirname(__file__), "models", "translation", "prompt", prompt_filename) + # translation フォルダ直下実行 + elif os_path.exists(os_path.join(os_path.dirname(__file__), "prompt", prompt_filename)): + prompt_path = os_path.join(os_path.dirname(__file__), "prompt", prompt_filename) + else: + raise FileNotFoundError(f"Prompt file not found: {prompt_filename}") + with open(prompt_path, "r", encoding="utf-8") as f: + return yaml.safe_load(f) + +class LMStudioClient: + """LM Studio Translation simple wrapper. + prompt/translation_lmstudio.yml から system_prompt / supported_languages を読み込む。 + """ + def __init__(self, base_url: str | None = None, root_path: str = None): + self.api_key = "lmstudio" + self.model = None + self.base_url = base_url # None の場合は公式エンドポイント + + prompt_config = _load_prompt_config(root_path) + self.supported_languages = prompt_config["supported_languages"] + self.prompt_template = prompt_config["system_prompt"] + + self.openai_llm = None + + def getBaseURL(self) -> str | None: + return self.base_url + + def setBaseURL(self, base_url: str | None) -> None: + result = _authentication_check(api_key=self.api_key, base_url=base_url) + if result: + self.base_url = base_url + return result + + def getModelList(self) -> list[str]: + return _get_available_text_models(api_key=self.api_key, base_url=self.base_url) if self.base_url else [] + + def getModel(self) -> str: + return self.model + + def setModel(self, model: str) -> bool: + if model in self.getModelList(): + self.model = model + return True + else: + return False + + def updateClient(self) -> None: + self.openai_llm = ChatOpenAI( + base_url=self.base_url, + model=self.model, + api_key=SecretStr(self.api_key), + streaming=False, + ) + + def translate(self, text: str, input_lang: str, output_lang: str) -> str: + system_prompt = self.prompt_template.format( + supported_languages=self.supported_languages, + input_lang=input_lang, + output_lang=output_lang, + ) + messages = [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": text}, + ] + + resp = self.openai_llm.invoke(messages) + content = "" + if isinstance(resp.content, str): + content = resp.content + elif isinstance(resp.content, list): + for part in resp.content: + if isinstance(part, str): + content += part + elif isinstance(part, dict) and "content" in part and isinstance(part["content"], str): + content += part["content"] + return content.strip() + +if __name__ == "__main__": + AUTH_KEY = "lm-studio" + client = LMStudioClient(base_url="http://192.168.68.110:1234/v1") + models = client.getModelList() + print(models) + # if models: + # print("Available models:", models) + # model = input("Select a model: ") + client.setModel("google/gemma-3n-e4b") + client.updateClient() + print(client.translate("こんにちは世界", "Japanese", "English")) \ No newline at end of file diff --git a/src-python/models/translation/translation_ollama.py b/src-python/models/translation/translation_ollama.py new file mode 100644 index 00000000..22ce3e4f --- /dev/null +++ b/src-python/models/translation/translation_ollama.py @@ -0,0 +1,115 @@ +import requests +from langchain_ollama import ChatOllama +import yaml +from os import path as os_path + + +def _authentication_check(base_url: str | None = None) -> bool: + """Check authentication for Ollama API. + """ + try: + response = requests.get(f"{base_url}/api/ping") + if response.status_code == 200: + return True + else: + return False + except Exception: + return False + +def _get_available_text_models(base_url: str | None = None) -> list[str]: + """Extract available text models from Ollama. + """ + response = requests.get(f"{base_url}/api/tags") + models = response.json()["models"] + + allowed_models = [] + for model in models: + allowed_models.append(model["name"]) + + allowed_models.sort() + return allowed_models + +def _load_prompt_config(root_path: str = None) -> dict: + prompt_filename = "translation_ollama.yml" + # PyInstaller 展開後 + if root_path and os_path.exists(os_path.join(root_path, "_internal", "prompt", prompt_filename)): + prompt_path = os_path.join(root_path, "_internal", "prompt", prompt_filename) + # src-python 直下実行 + elif os_path.exists(os_path.join(os_path.dirname(__file__), "models", "translation", "prompt", prompt_filename)): + prompt_path = os_path.join(os_path.dirname(__file__), "models", "translation", "prompt", prompt_filename) + # translation フォルダ直下実行 + elif os_path.exists(os_path.join(os_path.dirname(__file__), "prompt", prompt_filename)): + prompt_path = os_path.join(os_path.dirname(__file__), "prompt", prompt_filename) + else: + raise FileNotFoundError(f"Prompt file not found: {prompt_filename}") + with open(prompt_path, "r", encoding="utf-8") as f: + return yaml.safe_load(f) + +class OllamaClient: + """Ollama Translation simple wrapper. + prompt/translation_ollama.yml から system_prompt / supported_languages を読み込む。 + """ + def __init__(self, root_path: str = None): + self.model = None + self.base_url = "http://localhost:11434" + + prompt_config = _load_prompt_config(root_path) + self.supported_languages = prompt_config["supported_languages"] + self.prompt_template = prompt_config["system_prompt"] + + self.openai_llm = None + + def getModelList(self) -> list[str]: + if _authentication_check(self.base_url): + return _get_available_text_models(self.base_url) + return [] + + def getModel(self) -> str: + return self.model + + def setModel(self, model: str) -> bool: + if model in self.getModelList(): + self.model = model + return True + else: + return False + + def updateClient(self) -> None: + self.openai_llm = ChatOllama( + base_url=self.base_url, + model=self.model, + streaming=False, + ) + + def translate(self, text: str, input_lang: str, output_lang: str) -> str: + system_prompt = self.prompt_template.format( + supported_languages=self.supported_languages, + input_lang=input_lang, + output_lang=output_lang, + ) + messages = [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": text}, + ] + + resp = self.openai_llm.invoke(messages) + content = "" + if isinstance(resp.content, str): + content = resp.content + elif isinstance(resp.content, list): + for part in resp.content: + if isinstance(part, str): + content += part + elif isinstance(part, dict) and "content" in part and isinstance(part["content"], str): + content += part["content"] + return content.strip() + +if __name__ == "__main__": + client = OllamaClient() + models = client.getModelList() + if models: + print("Available models:", models) + model = input("Select a model: ") + client.setModel(model) + client.updateClient() + print(client.translate("こんにちは世界", "Japanese", "English")) \ No newline at end of file