diff options
| author | salmonik <salmonik@noreply.codeberg.org> | 2026-06-03 14:00:03 +0200 |
|---|---|---|
| committer | salmonik <salmonik@noreply.codeberg.org> | 2026-06-03 14:00:03 +0200 |
| commit | c027964952bd0d9700a9c83f72fb23bd5bffad3e (patch) | |
| tree | 1a7f31b08539288eab7fb58bd0ce3b14b2bb0194 /cogs | |
| parent | Merge pull request 'Voice related fixes, alongside simplifying assistant' (#5... (diff) | |
| parent | feat: add local API support for OpenAI compatible APIs (diff) | |
| download | senator-sperma-c027964952bd0d9700a9c83f72fb23bd5bffad3e.tar.gz senator-sperma-c027964952bd0d9700a9c83f72fb23bd5bffad3e.zip | |
Merge pull request 'feat: add local API support for OpenAI compatible APIs' (#6) from dev into main
Reviewed-on: https://codeberg.org/salmonik/senator-sperma/pulls/6
Diffstat (limited to '')
| -rw-r--r-- | cogs/ai.py | 75 | ||||
| -rw-r--r-- | cogs/assistant.py | 53 | ||||
| -rw-r--r-- | cogs/utils/llm.py | 62 |
3 files changed, 106 insertions, 84 deletions
| @@ -7,12 +7,14 @@ import os | |||
| 7 | import discord | 7 | import discord |
| 8 | from discord.ext import commands | 8 | from discord.ext import commands |
| 9 | import logging | 9 | import logging |
| 10 | import httpx | ||
| 11 | from gtts import gTTS | 10 | from gtts import gTTS |
| 12 | import io | 11 | import io |
| 12 | from cogs.utils.llm import llm_completion | ||
| 13 | 13 | ||
| 14 | logger = logging.getLogger(__name__) | 14 | logger = logging.getLogger(__name__) |
| 15 | 15 | ||
| 16 | _DEFAULT_SENATOR_MODEL = "cognitivecomputations/dolphin-mistral-24b-venice-edition:free" | ||
| 17 | |||
| 16 | class AI(commands.Cog): | 18 | class AI(commands.Cog): |
| 17 | def __init__(self, bot): | 19 | def __init__(self, bot): |
| 18 | self.bot = bot | 20 | self.bot = bot |
| @@ -29,48 +31,41 @@ class AI(commands.Cog): | |||
| 29 | ): | 31 | ): |
| 30 | await ctx.defer() | 32 | await ctx.defer() |
| 31 | 33 | ||
| 34 | model = ( | ||
| 35 | os.getenv("LLM_MODEL") | ||
| 36 | or os.getenv("OPENROUTER_MODEL") | ||
| 37 | or _DEFAULT_SENATOR_MODEL | ||
| 38 | ) | ||
| 39 | messages = [ | ||
| 40 | { | ||
| 41 | "role": "system", | ||
| 42 | "content": "You are Senator Steaven Sperma Armstrong from Metal Gear Rising Vengeance. \ | ||
| 43 | You are racist and you hate other minorities. Respond in plain text, no markdown tricks. \ | ||
| 44 | If the message is in Polish, respond in Polish. Respond in 100 words." | ||
| 45 | }, | ||
| 46 | { | ||
| 47 | "role": "user", | ||
| 48 | "content": text | ||
| 49 | } | ||
| 50 | ] | ||
| 32 | 51 | ||
| 33 | url = "https://openrouter.ai/api/v1/chat/completions" | 52 | response_text = await llm_completion(messages, model=model) |
| 34 | headers = { | ||
| 35 | "Authorization": f"Bearer {os.getenv('OPENROUTER_API_KEY')}", | ||
| 36 | "Content-Type": "application/json", | ||
| 37 | } | ||
| 38 | payload = { | ||
| 39 | "model": "cognitivecomputations/dolphin-mistral-24b-venice-edition:free", | ||
| 40 | "messages": [ | ||
| 41 | { | ||
| 42 | "role": "system", | ||
| 43 | "content": "You are Senator Steaven Sperma Armstrong from Metal Gear Rising Vengeance. \ | ||
| 44 | You are racist and you hate other minorities. Respond in plain text, no markdown tricks. \ | ||
| 45 | If the message is in Polish, respond in Polish. Respond in 100 words." | ||
| 46 | }, | ||
| 47 | { | ||
| 48 | "role": "user", | ||
| 49 | "content": text | ||
| 50 | } | ||
| 51 | ], | ||
| 52 | 53 | ||
| 53 | } | 54 | if response_text is None: |
| 54 | async with httpx.AsyncClient() as client: | 55 | await ctx.send_followup("bot siem zesroł") |
| 55 | response = await client.post(url=url, headers=headers, json=payload) | 56 | return |
| 56 | response_data = response.json() | ||
| 57 | logger.info(response_data) | ||
| 58 | if not response_data["choices"]: | ||
| 59 | await ctx.send_followup("bot siem zesroł") | ||
| 60 | else: | ||
| 61 | response_text = response_data["choices"][0]["message"]["content"] | ||
| 62 | if tts: | ||
| 63 | tts_obj = gTTS(text=response_text, lang='pl') | ||
| 64 | tts_buffer = io.BytesIO() | ||
| 65 | tts_obj.write_to_fp(tts_buffer) | ||
| 66 | tts_buffer.seek(0) | ||
| 67 | from cogs.voice import get_voice_manager | ||
| 68 | vm = get_voice_manager(self.bot) | ||
| 69 | channel = ctx.author.voice.channel | ||
| 70 | await vm.play_source(channel, discord.FFmpegPCMAudio(tts_buffer), interrupt=False) | ||
| 71 | 57 | ||
| 72 | else: | 58 | if tts: |
| 73 | await ctx.send_followup(response_text) | 59 | tts_obj = gTTS(text=response_text, lang='pl') |
| 60 | tts_buffer = io.BytesIO() | ||
| 61 | tts_obj.write_to_fp(tts_buffer) | ||
| 62 | tts_buffer.seek(0) | ||
| 63 | from cogs.voice import get_voice_manager | ||
| 64 | vm = get_voice_manager(self.bot) | ||
| 65 | channel = ctx.author.voice.channel | ||
| 66 | await vm.play_source(channel, discord.FFmpegPCMAudio(tts_buffer), interrupt=False) | ||
| 67 | else: | ||
| 68 | await ctx.send_followup(response_text) | ||
| 74 | 69 | ||
| 75 | 70 | ||
| 76 | 71 | ||
diff --git a/cogs/assistant.py b/cogs/assistant.py index fed7825..ffb664e 100644 --- a/cogs/assistant.py +++ b/cogs/assistant.py | |||
| @@ -5,9 +5,9 @@ | |||
| 5 | 5 | ||
| 6 | import discord | 6 | import discord |
| 7 | from discord.ext import commands | 7 | from discord.ext import commands |
| 8 | import httpx | ||
| 9 | import logging | 8 | import logging |
| 10 | import os | 9 | import os |
| 10 | from cogs.utils.llm import llm_completion | ||
| 11 | 11 | ||
| 12 | logger = logging.getLogger(__name__) | 12 | logger = logging.getLogger(__name__) |
| 13 | 13 | ||
| @@ -32,24 +32,20 @@ class Jarvis(commands.Cog): | |||
| 32 | 32 | ||
| 33 | @staticmethod | 33 | @staticmethod |
| 34 | async def assistant(message): | 34 | async def assistant(message): |
| 35 | headers = { | ||
| 36 | "Authorization": f"Bearer {os.getenv('OPENROUTER_API_KEY')}", | ||
| 37 | "Content-Type": "application/json" | ||
| 38 | } | ||
| 39 | |||
| 40 | chain = await Jarvis._build_message_chain(message) | 35 | chain = await Jarvis._build_message_chain(message) |
| 41 | messages = Jarvis._build_messages(message, chain) | 36 | messages = Jarvis._build_messages(message, chain) |
| 42 | payload = Jarvis._build_payload(messages) | ||
| 43 | 37 | ||
| 44 | response_data = await Jarvis._safe_api_call(payload, headers) | 38 | extra = None |
| 45 | if response_data is None: | 39 | api_url = os.getenv("LLM_API_URL", "https://openrouter.ai/api/v1/chat/completions") |
| 40 | if "openrouter" in api_url.lower(): | ||
| 41 | extra = {"plugins": [{"id": "response-healing"}]} | ||
| 42 | |||
| 43 | response_text = await llm_completion(messages, extra=extra) | ||
| 44 | if response_text is None: | ||
| 46 | await message.reply("Przepraszam, nie mogę teraz odpowiedzieć. Spróbuj ponownie później.") | 45 | await message.reply("Przepraszam, nie mogę teraz odpowiedzieć. Spróbuj ponownie później.") |
| 47 | return | 46 | return |
| 48 | 47 | ||
| 49 | response_message = Jarvis._parse_response(response_data) | 48 | await message.reply(response_text) |
| 50 | |||
| 51 | channel = message.channel | ||
| 52 | await message.reply(response_message) | ||
| 53 | 49 | ||
| 54 | @staticmethod | 50 | @staticmethod |
| 55 | async def _build_message_chain(message): | 51 | async def _build_message_chain(message): |
| @@ -93,38 +89,7 @@ class Jarvis(commands.Cog): | |||
| 93 | 89 | ||
| 94 | return messages | 90 | return messages |
| 95 | 91 | ||
| 96 | @staticmethod | ||
| 97 | def _build_payload(messages): | ||
| 98 | return { | ||
| 99 | "model": os.getenv("OPENROUTER_MODEL"), | ||
| 100 | "messages": messages, | ||
| 101 | "plugins": [{"id": "response-healing"}] | ||
| 102 | } | ||
| 103 | |||
| 104 | @staticmethod | ||
| 105 | async def _make_api_request(payload, headers): | ||
| 106 | async with httpx.AsyncClient(timeout=30.0) as client: | ||
| 107 | url = "https://openrouter.ai/api/v1/chat/completions" | ||
| 108 | response = await client.post(url=url, headers=headers, json=payload) | ||
| 109 | response_data = response.json() | ||
| 110 | logger.info(response_data) | ||
| 111 | return response_data | ||
| 112 | |||
| 113 | @staticmethod | ||
| 114 | async def _safe_api_call(payload, headers): | ||
| 115 | try: | ||
| 116 | return await Jarvis._make_api_request(payload, headers) | ||
| 117 | except httpx.HTTPError as e: | ||
| 118 | logger.error(f"API request failed: {e}") | ||
| 119 | return None | ||
| 120 | |||
| 121 | @staticmethod | ||
| 122 | def _parse_response(response_data): | ||
| 123 | content = response_data['choices'][0]['message']['content'] | ||
| 124 | |||
| 125 | logger.info(content) | ||
| 126 | 92 | ||
| 127 | return content | ||
| 128 | 93 | ||
| 129 | def setup(bot): | 94 | def setup(bot): |
| 130 | bot.add_cog(Jarvis(bot)) | 95 | bot.add_cog(Jarvis(bot)) |
diff --git a/cogs/utils/llm.py b/cogs/utils/llm.py new file mode 100644 index 0000000..2b05b12 --- /dev/null +++ b/cogs/utils/llm.py | |||
| @@ -0,0 +1,62 @@ | |||
| 1 | # This file is part of Senator Sperma | ||
| 2 | # Licensed under the GPL-3.0 License | ||
| 3 | # Copyright (C) 2026 Salmonik @ salmonik@tuta.io | ||
| 4 | # https://codeberg.org/salmonik/senator-sperma | ||
| 5 | |||
| 6 | import os | ||
| 7 | import httpx | ||
| 8 | import logging | ||
| 9 | |||
| 10 | logger = logging.getLogger(__name__) | ||
| 11 | |||
| 12 | BACKWARD_COMPAT_MSG = ( | ||
| 13 | "OPENROUTER_API_KEY/OPENROUTER_MODEL are deprecated. " | ||
| 14 | "Use LLM_API_KEY, LLM_MODEL instead." | ||
| 15 | ) | ||
| 16 | |||
| 17 | |||
| 18 | def _get_config(): | ||
| 19 | api_url = os.getenv("LLM_API_URL", "https://openrouter.ai/api/v1/chat/completions") | ||
| 20 | api_key = os.getenv("LLM_API_KEY") or os.getenv("OPENROUTER_API_KEY") or "" | ||
| 21 | model = os.getenv("LLM_MODEL") or os.getenv("OPENROUTER_MODEL") or "" | ||
| 22 | |||
| 23 | if os.getenv("OPENROUTER_API_KEY") and not os.getenv("LLM_API_KEY"): | ||
| 24 | logger.warning(BACKWARD_COMPAT_MSG) | ||
| 25 | if os.getenv("OPENROUTER_MODEL") and not os.getenv("LLM_MODEL"): | ||
| 26 | logger.warning(BACKWARD_COMPAT_MSG) | ||
| 27 | |||
| 28 | return api_url, api_key, model | ||
| 29 | |||
| 30 | |||
| 31 | async def llm_completion( | ||
| 32 | messages: list[dict], | ||
| 33 | model: str | None = None, | ||
| 34 | api_url: str | None = None, | ||
| 35 | api_key: str | None = None, | ||
| 36 | extra: dict | None = None, | ||
| 37 | timeout: float = 180.0, | ||
| 38 | ) -> str | None: | ||
| 39 | url = api_url or os.getenv("LLM_API_URL", "https://openrouter.ai/api/v1/chat/completions") | ||
| 40 | model = model or os.getenv("LLM_MODEL") or os.getenv("OPENROUTER_MODEL") or "" | ||
| 41 | api_key = api_key or os.getenv("LLM_API_KEY") or os.getenv("OPENROUTER_API_KEY") or "" | ||
| 42 | |||
| 43 | headers = {"Content-Type": "application/json"} | ||
| 44 | if api_key: | ||
| 45 | headers["Authorization"] = f"Bearer {api_key}" | ||
| 46 | |||
| 47 | payload: dict = {"model": model, "messages": messages} | ||
| 48 | if extra: | ||
| 49 | payload.update(extra) | ||
| 50 | |||
| 51 | verify = os.getenv("LLM_API_VERIFY", "true").lower() in ("true", "1", "yes") | ||
| 52 | |||
| 53 | try: | ||
| 54 | async with httpx.AsyncClient(timeout=timeout, verify=verify) as client: | ||
| 55 | resp = await client.post(url, headers=headers, json=payload) | ||
| 56 | resp.raise_for_status() | ||
| 57 | data = resp.json() | ||
| 58 | logger.info("LLM response: %s", data) | ||
| 59 | return data["choices"][0]["message"]["content"] | ||
| 60 | except httpx.HTTPError as e: | ||
| 61 | logger.error("LLM API request failed (url=%s, model=%s): %s", url, model, e, exc_info=True) | ||
| 62 | return None | ||
