diff --git a/README.md b/README.md index 3f215cc..45e4423 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,10 @@ To send a single query to an LLM, you can ask Neon to "ask Chat GPT " To start conversing with an LLM, ask to "talk to Chat GPT" and have all of your input sent to an LLM until you say goodbye or stop talking for a while. +Supported LLMs (subject to what your Neon Diana/Hana backend has deployed): +Chat GPT, FastChat, Claude, and Gemini. Name any of them in a request, +e.g. "ask Claude " or "talk to Gemini". + Enable fallback behavior by asking to "enable LLM fallback skill" or disable it by asking to "disable LLM fallback". diff --git a/__init__.py b/__init__.py index d640711..4374b6b 100644 --- a/__init__.py +++ b/__init__.py @@ -46,6 +46,8 @@ class LLM(Enum): GPT = "Chat GPT" FASTCHAT = "FastChat" + CLAUDE = "Claude" + GEMINI = "Gemini" class LLMSkill(FallbackSkill): @@ -53,7 +55,6 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.chat_history = dict() self._default_user = "local" - self._default_llm = LLM.FASTCHAT self.chatting = dict() self.register_entity_file("llm.entity") @@ -77,6 +78,23 @@ def chat_timeout_seconds(self): def fallback_enabled(self): return self.settings.get("fallback_enabled", False) + @property + def _default_llm(self) -> LLM: + """LLM used for the fallback handler and unrecognized requests. + + Configurable via the `default_llm` setting (spoken name or enum name, + e.g. "chat gpt", "Chat GPT", or "GPT"); defaults to FastChat. + """ + configured = self.settings.get("default_llm") + if not configured: + return LLM.FASTCHAT + requested = str(configured).strip().lower() + for llm in LLM: + if requested in (llm.value.lower(), llm.name.lower()): + return llm + LOG.warning(f"Unrecognized default_llm={configured!r}; using FastChat") + return LLM.FASTCHAT + @fallback_handler(85) def fallback_llm(self, message): if not self.fallback_enabled: @@ -86,7 +104,8 @@ def fallback_llm(self, message): LOG.info(f"Getting LLM response to: {utterance}") user = get_message_user(message) or self._default_user - def _threaded_get_response(utt, usr): + def _threaded_get_response(utt, usr, message): + # `message` required to resolve response routing in `speak` answer = self._get_llm_response(utt, usr, self._default_llm) if not answer: LOG.info("No fallback response") @@ -94,7 +113,8 @@ def _threaded_get_response(utt, usr): self.speak(answer) # TODO: Speak filler? - Thread(target=_threaded_get_response, args=(utterance, user), daemon=True).start() + Thread(target=_threaded_get_response, + args=(utterance, user, message), daemon=True).start() return True @intent_handler("enable_fallback.intent") @@ -177,6 +197,10 @@ def _get_llm_response(self, query: str, user: str, llm: LLM) -> str: endpoint = "chatgpt" elif llm == LLM.FASTCHAT: endpoint = "fastchat" + elif llm == LLM.CLAUDE: + endpoint = "claude" + elif llm == LLM.GEMINI: + endpoint = "gemini" else: raise ValueError(f"Expected LLM, got: {llm}") self.chat_history.setdefault(user, list()) @@ -184,8 +208,9 @@ def _get_llm_response(self, query: str, user: str, llm: LLM) -> str: resp = resp.get("response") or "" if resp: - username = "user" if user == self._default_user else user - self.chat_history[user].append((username, query)) + # History roles must be the literal "user"/"llm" the backend maps; + # the per-user key identifies the history, not the message role. + self.chat_history[user].append(("user", query)) self.chat_history[user].append(("llm", resp)) LOG.debug(f"Got LLM response: {resp}") return resp @@ -196,6 +221,10 @@ def _get_requested_llm(self, message: Message) -> LLM: llm = LLM.GPT elif self.voc_match(request, "fastchat"): llm = LLM.FASTCHAT + elif self.voc_match(request, "claude"): + llm = LLM.CLAUDE + elif self.voc_match(request, "gemini"): + llm = LLM.GEMINI else: LOG.warning(f"No valid LLM in request: {request}") llm = LLM.GPT diff --git a/locale/en-us/intent/ask_llm.intent b/locale/en-us/intent/ask_llm.intent index 3d672c9..61e003f 100644 --- a/locale/en-us/intent/ask_llm.intent +++ b/locale/en-us/intent/ask_llm.intent @@ -1 +1 @@ -(ask|tell|task|half|sjet|asked|as) (chatgpt|chat gpt|chat g p t|chat gpg|chat gptc|jackgpt|jack gpt|jack chi pt|fast chat|fastchat|chat gbt|chatgbt|chat gpt4|chatgpt4|a language model|an llm) {question} +(ask|tell|task|half|sjet|asked|as) (chatgpt|chat gpt|chat g p t|chat gpg|chat gptc|jackgpt|jack gpt|jack chi pt|fast chat|fastchat|chat gbt|chatgbt|chat gpt4|chatgpt4|claude|anthropic|gemini|bard|a language model|an llm) {question} diff --git a/locale/en-us/intent/chat_with_llm.intent b/locale/en-us/intent/chat_with_llm.intent index 1e35059..0793811 100644 --- a/locale/en-us/intent/chat_with_llm.intent +++ b/locale/en-us/intent/chat_with_llm.intent @@ -1 +1 @@ -(i want to |)(chat|talk|converse|start a conversation) (to|with) (chatgpt|chat gpt|chat g p t|chat gpg|chat gptc|jackgpt|jack gpt|jack chi pt|fast chat|fastchat|chat gbt|chatgbt|chat gpt4|chatgpt4|a language model|an llm) +(i want to |)(chat|talk|converse|start a conversation) (to|with) (chatgpt|chat gpt|chat g p t|chat gpg|chat gptc|jackgpt|jack gpt|jack chi pt|fast chat|fastchat|chat gbt|chatgbt|chat gpt4|chatgpt4|claude|anthropic|gemini|bard|a language model|an llm) diff --git a/locale/en-us/intent/llm.entity b/locale/en-us/intent/llm.entity index 0b3b8cb..9748f58 100644 --- a/locale/en-us/intent/llm.entity +++ b/locale/en-us/intent/llm.entity @@ -10,3 +10,9 @@ chat gbt chatgbt chat gpt4 chatgpt4 +fastchat +fast chat +claude +anthropic +gemini +bard diff --git a/locale/en-us/vocab/claude.voc b/locale/en-us/vocab/claude.voc new file mode 100644 index 0000000..093c3e2 --- /dev/null +++ b/locale/en-us/vocab/claude.voc @@ -0,0 +1,3 @@ +claude +claud +anthropic \ No newline at end of file diff --git a/locale/en-us/vocab/gemini.voc b/locale/en-us/vocab/gemini.voc new file mode 100644 index 0000000..16d8e27 --- /dev/null +++ b/locale/en-us/vocab/gemini.voc @@ -0,0 +1,5 @@ +gemini +gemeni +bard +google's llm +google llm \ No newline at end of file diff --git a/locale/uk-ua/intent/ask_llm.intent b/locale/uk-ua/intent/ask_llm.intent index 5e6930c..a7fa114 100644 --- a/locale/uk-ua/intent/ask_llm.intent +++ b/locale/uk-ua/intent/ask_llm.intent @@ -1 +1 @@ -(запитай|скажи|попроси|задай питання|запитати|спитай|дізнайся у) (chatgpt|chat gpt|chat g p t|chat gpg|chat gptc|jackgpt|jack gpt|чат gpt|чатgpt|штучний інтелект|мовну модель|llm) {question} +(запитай|скажи|попроси|задай питання|запитати|спитай|дізнайся у) (chatgpt|chat gpt|chat g p t|chat gpg|chat gptc|jackgpt|jack gpt|чат gpt|чатgpt|claude|клод|gemini|джеміні|bard|штучний інтелект|мовну модель|llm) {question} diff --git a/locale/uk-ua/intent/chat_with_llm.intent b/locale/uk-ua/intent/chat_with_llm.intent index 53456ca..e47a18a 100644 --- a/locale/uk-ua/intent/chat_with_llm.intent +++ b/locale/uk-ua/intent/chat_with_llm.intent @@ -1 +1 @@ -(я хочу |)(поспілкуватися|поговорити|почати розмову|спілкуватися) (з|із) (chatgpt|chat gpt|chat g p t|chat gpg|chat gptc|jackgpt|jack gpt|чат gpt|чатgpt|штучним інтелектом|мовною моделлю|llm) +(я хочу |)(поспілкуватися|поговорити|почати розмову|спілкуватися) (з|із) (chatgpt|chat gpt|chat g p t|chat gpg|chat gptc|jackgpt|jack gpt|чат gpt|чатgpt|claude|клод|gemini|джеміні|bard|штучним інтелектом|мовною моделлю|llm) diff --git a/locale/uk-ua/intent/llm.entity b/locale/uk-ua/intent/llm.entity index 82ebe3f..7bfc5fc 100644 --- a/locale/uk-ua/intent/llm.entity +++ b/locale/uk-ua/intent/llm.entity @@ -14,3 +14,9 @@ chatgpt4 чат gpt штучний інтелект мовна модель +claude +anthropic +клод +gemini +bard +джеміні diff --git a/locale/uk-ua/vocab/claude.voc b/locale/uk-ua/vocab/claude.voc new file mode 100644 index 0000000..fedf861 --- /dev/null +++ b/locale/uk-ua/vocab/claude.voc @@ -0,0 +1,5 @@ +claude +anthropic +клод +клауд +антропік \ No newline at end of file diff --git a/locale/uk-ua/vocab/gemini.voc b/locale/uk-ua/vocab/gemini.voc new file mode 100644 index 0000000..a8cc8ac --- /dev/null +++ b/locale/uk-ua/vocab/gemini.voc @@ -0,0 +1,5 @@ +gemini +bard +джеміні +джемені +бард \ No newline at end of file diff --git a/test/test_resources.yaml b/test/test_resources.yaml index 4114424..ed98721 100644 --- a/test/test_resources.yaml +++ b/test/test_resources.yaml @@ -9,6 +9,8 @@ vocab: - exit - chat_gpt - fastchat + - claude + - gemini # dialog is .dialog file basenames (case-sensitive) dialog: - end_chat diff --git a/test/test_skill.py b/test/test_skill.py index ea5e0a4..a65357a 100644 --- a/test/test_skill.py +++ b/test/test_skill.py @@ -124,6 +124,43 @@ def test_handle_email_chat_history(self): self.skill._send_email = real_send_email + def test_get_requested_llm(self): + cases = { + "ask chat gpt something": LLM.GPT, + "talk to fastchat": LLM.FASTCHAT, + "ask claude something": LLM.CLAUDE, + "chat with gemini": LLM.GEMINI, + } + for utterance, expected in cases.items(): + message = Message("test", {"utterance": utterance}) + self.assertEqual(self.skill._get_requested_llm(message), expected, + utterance) + # Unrecognized LLM falls back to GPT + message = Message("test", {"utterance": "ask the robot something"}) + self.assertEqual(self.skill._get_requested_llm(message), LLM.GPT) + + def test_get_llm_response_endpoints(self): + from unittest.mock import patch + expected = { + LLM.GPT: "chatgpt", + LLM.FASTCHAT: "fastchat", + LLM.CLAUDE: "claude", + LLM.GEMINI: "gemini", + } + real_history = self.skill.chat_history + try: + for llm, endpoint in expected.items(): + self.skill.chat_history = dict() + with patch("skill_fallback_llm.request_backend", + return_value={"response": "hi"}) as mock_backend: + resp = self.skill._get_llm_response("hello", "endpoint_user", + llm) + self.assertEqual(resp, "hi") + self.assertEqual(mock_backend.call_args[0][0], + f"/llm/{endpoint}") + finally: + self.skill.chat_history = real_history + def test_converse(self): # TODO pass