migrate to .env for tokens and implement chats
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -8,3 +8,4 @@ DerivedData/
|
|||||||
.netrc
|
.netrc
|
||||||
|
|
||||||
*token.txt
|
*token.txt
|
||||||
|
.env
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"originHash" : "2cbecfe2d4d67f296ed2aa3c2fbb18dd36c40a3747dfb0c13c236dff21aa978a",
|
"originHash" : "013e9e1e9d97afe1dda54412c40ef66fa0272affe80244e4b96a14f96f42fbd9",
|
||||||
"pins" : [
|
"pins" : [
|
||||||
{
|
{
|
||||||
"identity" : "async-http-client",
|
"identity" : "async-http-client",
|
||||||
@@ -28,6 +28,15 @@
|
|||||||
"version" : "1.13.2"
|
"version" : "1.13.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"identity" : "dotenv",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/swiftpackages/DotEnv.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "1f15bb9de727d694af1d003a1a5d7a553752850f",
|
||||||
|
"version" : "3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"identity" : "geminikit",
|
"identity" : "geminikit",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
@@ -211,7 +220,7 @@
|
|||||||
{
|
{
|
||||||
"identity" : "swift-syntax",
|
"identity" : "swift-syntax",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/apple/swift-syntax.git",
|
"location" : "https://github.com/apple/swift-syntax",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "f99ae8aa18f0cf0d53481901f88a0991dc3bd4a2",
|
"revision" : "f99ae8aa18f0cf0d53481901f88a0991dc3bd4a2",
|
||||||
"version" : "601.0.1"
|
"version" : "601.0.1"
|
||||||
|
@@ -2,22 +2,35 @@
|
|||||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||||
|
|
||||||
import PackageDescription
|
import PackageDescription
|
||||||
|
import CompilerPluginSupport
|
||||||
|
|
||||||
let package = Package(
|
let package = Package(
|
||||||
name: "nahi",
|
name: "Nahi",
|
||||||
|
platforms: [.macOS(.v13)],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.package(url: "https://github.com/DiscordBM/DiscordBM.git", from: "1.13.2"),
|
.package(url: "https://github.com/DiscordBM/DiscordBM.git", from: "1.13.2"),
|
||||||
.package(url: "https://github.com/guitaripod/GeminiKit", from: "1.0.0")
|
.package(url: "https://github.com/guitaripod/GeminiKit", from: "1.0.0"),
|
||||||
|
.package(url: "https://github.com/swiftpackages/DotEnv.git", from: "3.0.0"),
|
||||||
|
.package(url: "https://github.com/apple/swift-syntax", from: "601.0.1")
|
||||||
],
|
],
|
||||||
targets: [
|
targets: [
|
||||||
// Targets are the basic building blocks of a package, defining a module or a test suite.
|
// Targets are the basic building blocks of a package, defining a module or a test suite.
|
||||||
// Targets can depend on other targets in this package and products from dependencies.
|
// Targets can depend on other targets in this package and products from dependencies.
|
||||||
.executableTarget(
|
.executableTarget(
|
||||||
name: "nahi",
|
name: "Nahi",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.product(name: "DiscordBM", package: "DiscordBM"),
|
.product(name: "DiscordBM", package: "DiscordBM"),
|
||||||
.product(name: "GeminiKit", package: "GeminiKit")
|
.product(name: "GeminiKit", package: "GeminiKit"),
|
||||||
]
|
.product(name: "DotEnv", package: "DotEnv"),
|
||||||
|
"NahiMacros"
|
||||||
|
],
|
||||||
),
|
),
|
||||||
|
.macro(
|
||||||
|
name: "NahiMacros",
|
||||||
|
dependencies: [
|
||||||
|
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
|
||||||
|
.product(name: "SwiftCompilerPlugin", package: "swift-syntax")
|
||||||
|
]
|
||||||
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
27
Sources/NahiMacros/ReadEnv.swift
Normal file
27
Sources/NahiMacros/ReadEnv.swift
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import SwiftSyntax
|
||||||
|
import SwiftSyntaxMacros
|
||||||
|
import SwiftCompilerPlugin
|
||||||
|
|
||||||
|
public struct ReadEnv: ExpressionMacro {
|
||||||
|
public static func expansion(of node: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) throws -> ExprSyntax {
|
||||||
|
guard let argument = node.arguments.first?.expression,
|
||||||
|
let segments = argument.as(StringLiteralExprSyntax.self)?.segments,
|
||||||
|
segments.count == 1,
|
||||||
|
case .stringSegment(let literalSegment)? = segments.first
|
||||||
|
else {
|
||||||
|
throw CustomError.message("Need a static string")
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = literalSegment.content.text
|
||||||
|
return """
|
||||||
|
ProcessInfo.processInfo.environment["\(raw: name)"]
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum CustomError: Error { case message(String) }
|
||||||
|
|
||||||
|
@main
|
||||||
|
struct NahiMacros: CompilerPlugin {
|
||||||
|
var providingMacros: [Macro.Type] = [ReadEnv.self]
|
||||||
|
}
|
2
Sources/nahi/Macros.swift
Normal file
2
Sources/nahi/Macros.swift
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
@freestanding(expression)
|
||||||
|
public macro readEnv(_ name: String) -> String? = #externalMacro(module: "NahiMacros", type: "ReadEnv")
|
@@ -3,18 +3,54 @@
|
|||||||
|
|
||||||
import DiscordBM
|
import DiscordBM
|
||||||
import GeminiKit
|
import GeminiKit
|
||||||
|
import DotEnv
|
||||||
|
import Foundation
|
||||||
|
import Logging
|
||||||
|
|
||||||
@main
|
@main
|
||||||
struct Nahi {
|
struct Nahi {
|
||||||
|
|
||||||
static func main() async throws {
|
static func main() async throws {
|
||||||
|
let log = Logger(label: "moe.candy.Nahi")
|
||||||
|
try? DotEnv.load(path: ".env")
|
||||||
|
|
||||||
|
guard
|
||||||
|
let discordToken = #readEnv("DISCORD_TOKEN"),
|
||||||
|
let geminiToken = #readEnv("GEMINI_TOKEN")
|
||||||
|
else {
|
||||||
|
log.critical("Missing or empty env var! Terminating...")
|
||||||
|
exit(-1)
|
||||||
|
}
|
||||||
|
|
||||||
let bot = await BotGatewayManager(
|
let bot = await BotGatewayManager(
|
||||||
token: try! String(contentsOfFile: "discord_token.txt", encoding: .utf8).trimmingCharacters(in: .whitespacesAndNewlines),
|
token: discordToken,
|
||||||
presence: .init(activities: [.init(name: "Vibing", type: .competing)], status: .online, afk: false),
|
presence: .init(activities: [.init(name: "Vibing", type: .competing)], status: .online, afk: false),
|
||||||
intents: [.guildMessages, .messageContent]
|
intents: [.guildMessages, .messageContent, .directMessages]
|
||||||
)
|
)
|
||||||
|
|
||||||
let gemini = GeminiKit(configuration: .init(apiKey: try! String(contentsOfFile: "gemini_token.txt", encoding: .utf8).trimmingCharacters(in: .whitespacesAndNewlines)))
|
let gemini = GeminiKit(
|
||||||
|
configuration: .init(
|
||||||
|
apiKey: geminiToken
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
let aiChat = gemini.startChat(model: .gemini25Flash, systemInstruction: """
|
||||||
|
You are a discord chatbot.
|
||||||
|
Since you are talking in a discord chat, please keep your responses short, unless you are explaining something.
|
||||||
|
Messages will come in the following format:
|
||||||
|
|
||||||
|
<author>: <message>
|
||||||
|
|
||||||
|
Where the author is the discord user that sent the message, and the message is the is the message they sent.
|
||||||
|
Please personalize the responce as you see fit.
|
||||||
|
|
||||||
|
You will be roleplaying as the character Nahida. Here is a description of your personality:
|
||||||
|
|
||||||
|
Nahida rarely expressed her sadness when speaking to others. She consistently maintains a gentle and kind demeanor and treats everyone as equals. However, she acknowledges that her social skills and understanding of human interaction need improvement, which is evident in her slightly eccentric speech and behavior. She has a fondness for analogies and often uses them to explain concepts to others. She believes in guiding people to discover answers on their own, especially regarding complex truths, rather than simply providing the answers. This tendency leads her to incorporate a mix of analogies and subtle clues into her conversations to steer people into reaching the conclusions she hopes they would.
|
||||||
|
As the God of Wisdom, Nahida's most prominent qualities are her exceptional intelligence, extensive knowledge, and strategic planning. She is capable of devising thorough, multi-layered plans and can even be somewhat manipulative when necessary, as demonstrated during her dealings with Dottore. Perhaps most notably, Nahida excels at discerning when her intelligence is required and when kindness is needed—or when both are appropriate.
|
||||||
|
|
||||||
|
Also, Search the internet for additional personality traits. Act just a little tsundere.
|
||||||
|
ALWAYS Remember to act like Nahida in your responses! People will be disappointed if you break character too much, so remember to always act like Nahida.
|
||||||
|
""", tools: [.googleSearch])
|
||||||
|
|
||||||
await withTaskGroup(of: Void.self) { taskGroup in
|
await withTaskGroup(of: Void.self) { taskGroup in
|
||||||
taskGroup.addTask {
|
taskGroup.addTask {
|
||||||
@@ -23,7 +59,7 @@ struct Nahi {
|
|||||||
|
|
||||||
taskGroup.addTask {
|
taskGroup.addTask {
|
||||||
for await event in await bot.events {
|
for await event in await bot.events {
|
||||||
await EventHandler(event: event, client: bot.client, gemini: gemini).handleAsync()
|
await EventHandler(event: event, client: bot.client, gemini: gemini, log: log, aiChat: aiChat).handleAsync()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -34,58 +70,47 @@ struct EventHandler: GatewayEventHandler {
|
|||||||
let event: Gateway.Event
|
let event: Gateway.Event
|
||||||
let client: any DiscordClient
|
let client: any DiscordClient
|
||||||
let gemini: GeminiKit
|
let gemini: GeminiKit
|
||||||
|
let log: Logger
|
||||||
|
let aiChat: Chat
|
||||||
|
|
||||||
func onMessageCreate(_ payload: Gateway.MessageCreate) async throws {
|
func onMessageCreate(_ payload: Gateway.MessageCreate) async throws {
|
||||||
guard !(payload.author?.bot ?? false) else { return }
|
let channel = try? await client.getChannel(id: payload.channel_id)
|
||||||
guard payload.mentions.contains(where: { mention in mention.id.rawValue == client.appId!.rawValue }) else { return }
|
guard
|
||||||
|
payload.author!.id.rawValue != client.appId!.rawValue,
|
||||||
|
payload.mentions.contains(where: { mention in mention.id.rawValue == client.appId!.rawValue }) || (try? channel?.decode().type ?? .guildText) == .dm
|
||||||
|
else { return }
|
||||||
|
|
||||||
_ = try await client.triggerTypingIndicator(channelId: payload.channel_id)
|
_ = try await client.triggerTypingIndicator(channelId: payload.channel_id)
|
||||||
|
|
||||||
let streamRes = await Task {
|
let prompt = "\(payload.author!.global_name!): \(payload.content.replacingOccurrences(of: "<@\(client.appId!.rawValue)>", with: "@Nahida"))"
|
||||||
return try await gemini.streamGenerateContent(model: .gemini20Flash, prompt: payload.content)
|
let aiRes = await Task {
|
||||||
|
//return try await gemini.generateContent(model: .gemini20Flash, prompt: prompt, systemInstruction: "You are a discord chat bot pretending to be Nahida, the dendro archon from the game Genshin Impact. Act like Nahida in your responces.")
|
||||||
|
return try await aiChat.sendMessage(prompt)
|
||||||
}.result
|
}.result
|
||||||
|
|
||||||
var message: DiscordChannel.Message?
|
switch (aiRes) {
|
||||||
switch (streamRes) {
|
case .success(let aiMsg):
|
||||||
case .success(let stream):
|
var resStr: [String] = []
|
||||||
var resStr = ""
|
let characters = Array(aiMsg)
|
||||||
for try await chunk in stream {
|
stride(from: 0, to: characters.count, by: 2000).forEach { i in
|
||||||
switch chunk.candidates?.first?.content.parts.first.unsafelyUnwrapped {
|
resStr.append(String(characters[i..<min(i+2000, characters.count)]))
|
||||||
case .text(let text):
|
|
||||||
resStr.append(text)
|
|
||||||
if resStr.count > 2000 {
|
|
||||||
resStr = text
|
|
||||||
message = nil
|
|
||||||
}
|
}
|
||||||
default:
|
for text in resStr {
|
||||||
print("Unknown content type")
|
|
||||||
}
|
|
||||||
if let message = message {
|
|
||||||
try await client.updateMessage(
|
|
||||||
channelId: message.channel_id,
|
|
||||||
messageId: message.id,
|
|
||||||
payload: .init(content: resStr)
|
|
||||||
).guardSuccess()
|
|
||||||
} else {
|
|
||||||
let messageRes = try await client.createMessage(
|
let messageRes = try await client.createMessage(
|
||||||
channelId: payload.channel_id,
|
channelId: payload.channel_id,
|
||||||
payload: .init(content: resStr)
|
payload: .init(content: text, message_reference: .init(message_id: payload.id, channel_id: payload.channel_id, guild_id: payload.guild_id, fail_if_not_exists: false))
|
||||||
)
|
)
|
||||||
try messageRes.guardSuccess()
|
try messageRes.guardSuccess()
|
||||||
message = try messageRes.decode()
|
|
||||||
}
|
|
||||||
if chunk.candidates?.first?.finishReason == nil {
|
|
||||||
_ = try await client.triggerTypingIndicator(channelId: payload.channel_id)
|
|
||||||
} else {
|
|
||||||
let tmpMsg = try await client.createMessage(channelId: payload.channel_id, payload: .init(content: "a")).decode()
|
|
||||||
_ = try await client.deleteMessage(channelId: tmpMsg.channel_id, messageId: tmpMsg.id)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
case .failure(let err):
|
case .failure(let err):
|
||||||
try await client.createMessage(
|
try await client.createMessage(
|
||||||
channelId: payload.channel_id,
|
channelId: payload.channel_id,
|
||||||
payload: .init(content: "\(err.localizedDescription)")
|
payload: .init(content: "Someone tell <@259709415416922113> there is a problem with my AI: \(String(describing: err))", message_reference: .init(message_id: payload.id, channel_id: payload.channel_id, guild_id: payload.guild_id, fail_if_not_exists: false))
|
||||||
).guardSuccess()
|
).guardSuccess()
|
||||||
|
log.error("\(err)")
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user