Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds Chat Message History and Connects to the NPC Chat API #3679

Merged
merged 6 commits into from
Mar 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions examples/simple-genai-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,21 @@ each request as part of the GenAIRequest structure. The default values for `GenA
to start the chat. The default values for the prompt is an empty string. `NumChats` is the number of
requests made to the `SimEndpoint` and `GenAiEndpoint`. The default value for is `NumChats` is `1`.

If you want to set up the chat with the npc-chat-api from the [Google for Games GenAI](https://github.com/googleforgames/GenAI-quickstart/genai/api/npc_chat_api)
you will need the Game Servers on the same cluster as the GenAI Inference Server. Set either the
`GenAiEndpoint` or `SimEndpoint` to the NPC service `"http://npc-chat-api.genai.svc.cluster.local:80"`.
Set whichever endpoint the pointing to the NPC service to be, either the `GenAiNpc` or `SimNpc`,
to be `"true"`. The `GenAIRequest` to the NPC endpoint only sends the message (prompt), so any
additional context outside of the prompt is ignored. `FromID` is the entity sending messages to NPC,
and `ToID` is the entity receiving the message (the NPC ID).
```
type NPCRequest struct {
Msg string `json:"message,omitempty"`
FromId int `json:"from_id,omitempty"`
ToId int `json:"to_id,omitempty"`
}
```

## Running the Game Server

Once you have modified the `gameserver_autochat.yaml` or `gameserver_manualchat.yaml` to use your
Expand Down
12 changes: 6 additions & 6 deletions examples/simple-genai-server/gameserver_autochat.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,20 @@ spec:
image: us-docker.pkg.dev/agones-images/examples/simple-genai-game-server:0.1
# imagePullPolicy: Always # add for development
env:
- name: GenAiEndpoint
- name: GEN_AI_ENDPOINT
# Replace with your GenAI and Sim inference servers' endpoint addresses. If the game
# server is in the same cluster as your inference server you can also use the k8s
# service discovery such as value: "http://vertex-chat-api.genai.svc.cluster.local:80"
value: "http://192.1.1.2/genai/chat"
- name: SimEndpoint
- name: SIM_ENDPOINT
value: "http://192.1.1.2/genai/chat"
- name: SimContext
- name: SIM_CONTEXT
value: "You are buying a car"
- name: GenAiContext
- name: GEN_AI_CONTEXT
value: "You are a car salesperson"
- name: Prompt
- name: PROMPT
value: "I would like to buy a car"
- name: NumChats
- name: NUM_CHATS
value: "50"
resources:
requests:
Expand Down
4 changes: 2 additions & 2 deletions examples/simple-genai-server/gameserver_manualchat.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,12 @@ spec:
image: us-docker.pkg.dev/agones-images/examples/simple-genai-game-server:0.1
# imagePullPolicy: Always # add for development
env:
- name: GenAiEndpoint
- name: GEN_AI_ENDPOINT
# Replace with your GenAI server's endpoint address. If the game server is in the
# same cluster as your inference server you can also use the k8s service discovery
# such as value: "http://vertex-chat-api.genai.svc.cluster.local:80"
value: "http://192.1.1.2/genai/chat"
- name: GenAiContext
- name: GEN_AI_CONTEXT
# Context is optional, and will be sent along with each post request
value: "You are a car salesperson"
resources:
Expand Down
66 changes: 66 additions & 0 deletions examples/simple-genai-server/gameserver_npcchat.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
---
# Copyright 2024 Google LLC All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
apiVersion: agones.dev/v1
kind: GameServer
metadata:
name: gen-ai-server-npc
spec:
ports:
- name: default
portPolicy: Dynamic
containerPort: 7654
protocol: TCP
template:
spec:
containers:
- name: simple-genai-game-server
image: us-docker.pkg.dev/agones-images/examples/simple-genai-game-server:0.1
# imagePullPolicy: Always # add for development
env:
- name: GEN_AI_ENDPOINT
# Use the service endpoint address when running in the same cluster as the inference server.
# TODO (igooch): Change this to the `/genai/npc-chat` endpoint when it's properly plumbed in the inference server
value: "http://npc-chat-api.genai.svc.cluster.local:80"
igooch marked this conversation as resolved.
Show resolved Hide resolved
# GenAiContext is not passed to the npc-chat-api endpoint.
- name: GEN_AI_NPC # False by default. Use GEN_AI_NPC "true" when using the npc-chat-api as the GEN_AI_ENDPOINT.
value: "true"
- name: FROM_ID # Default is "2".
value: "2"
- name: TO_ID # Default is "1".
value: "1"
- name: SIM_ENDPOINT
value: "http://192.1.1.2/genai/chat"
- name: SIM_CONTEXT
value: "Ask questions about one of the following: What happened here? Where were you during the earthquake? Do you have supplies?"
- name: SIM_NPC
value: "false" # False by default. Use SIM_NPC "true" when using the npc-chat-api as the SIM_ENDPOINT.
- name: PROMPT
value: "Hello"
- name: NUM_CHATS
value: "50"
resources:
requests:
memory: 64Mi
cpu: 20m
limits:
memory: 64Mi
cpu: 20m
# Schedule onto the game server node pool when running in the same cluster as the inference server.
# tolerations:
# - key: "agones.dev/role"
# value: "gameserver"
# effect: "NoExecute"
# nodeSelector:
# agones.dev/role: gameserver
131 changes: 107 additions & 24 deletions examples/simple-genai-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,33 +45,65 @@ func main() {
simEndpoint := flag.String("SimEndpoint", "", "The full base URL to send API requests to simulate user input")
simContext := flag.String("SimContext", "", "Context for the Sim endpoint")
numChats := flag.Int("NumChats", 1, "Number of back and forth chats between the sim and genAI")
genAiNpc := flag.Bool("GenAiNpc", false, "Set to true if the GenAIEndpoint is the npc-chat-api endpoint")
simNpc := flag.Bool("SimNpc", false, "Set to true if the SimEndpoint is the npc-chat-api endpoint")
fromId := flag.Int("FromID", 2, "Entity sending messages to the npc-chat-api")
toId := flag.Int("ToID", 1, "Entity receiving messages on the npc-chat-api (the NPC's ID)")

flag.Parse()
if ep := os.Getenv("PORT"); ep != "" {
port = &ep
}
if sc := os.Getenv("SimContext"); sc != "" {
if sc := os.Getenv("SIM_CONTEXT"); sc != "" {
simContext = &sc
}
if gac := os.Getenv("GenAiContext"); gac != "" {
if gac := os.Getenv("GEN_AI_CONTEXT"); gac != "" {
genAiContext = &gac
}
if p := os.Getenv("Prompt"); p != "" {
if p := os.Getenv("PROMPT"); p != "" {
prompt = &p
}
if se := os.Getenv("SimEndpoint"); se != "" {
if se := os.Getenv("SIM_ENDPOINT"); se != "" {
simEndpoint = &se
}
if gae := os.Getenv("GenAiEndpoint"); gae != "" {
if gae := os.Getenv("GEN_AI_ENDPOINT"); gae != "" {
genAiEndpoint = &gae
}
if nc := os.Getenv("NumChats"); nc != "" {
if nc := os.Getenv("NUM_CHATS"); nc != "" {
num, err := strconv.Atoi(nc)
if err != nil {
log.Fatalf("Could not parse NumChats: %v", err)
}
numChats = &num
}
if gan := os.Getenv("GEN_AI_NPC"); gan != "" {
gnpc, err := strconv.ParseBool(gan)
if err != nil {
log.Fatalf("Could parse GenAiNpc: %v", err)
}
genAiNpc = &gnpc
}
if sn := os.Getenv("SIM_NPC"); sn != "" {
snpc, err := strconv.ParseBool(sn)
if err != nil {
log.Fatalf("Could parse GenAiNpc: %v", err)
}
simNpc = &snpc
}
if fid := os.Getenv("FROM_ID"); fid != "" {
num, err := strconv.Atoi(fid)
if err != nil {
log.Fatalf("Could not parse FromId: %v", err)
}
fromId = &num
}
if tid := os.Getenv("TO_ID"); tid != "" {
num, err := strconv.Atoi(tid)
if err != nil {
log.Fatalf("Could not parse ToId: %v", err)
}
toId = &num
}

log.Print("Creating SDK instance")
s, err := sdk.NewSDK()
Expand All @@ -85,14 +117,14 @@ func main() {
var simConn *connection
if *simEndpoint != "" {
log.Printf("Creating Sim Client at endpoint %s", *simEndpoint)
simConn = initClient(*simEndpoint, *simContext, "Sim")
simConn = initClient(*simEndpoint, *simContext, "Sim", *simNpc, *fromId, *toId)
}

if *genAiEndpoint == "" {
log.Fatalf("GenAiEndpoint must be specified")
}
log.Printf("Creating GenAI Client at endpoint %s", *genAiEndpoint)
genAiConn := initClient(*genAiEndpoint, *genAiContext, "GenAI")
genAiConn := initClient(*genAiEndpoint, *genAiContext, "GenAI", *genAiNpc, *fromId, *toId)

log.Print("Marking this server as ready")
if err := s.Ready(); err != nil {
Expand All @@ -108,7 +140,8 @@ func main() {
var wg sync.WaitGroup
// TODO: Add flag for creating X number of chats
wg.Add(1)
go autonomousChat(*prompt, genAiConn, simConn, *numChats, &wg, sigCtx)
chatHistory := []Message{{Author: simConn.name, Content: *prompt}}
go autonomousChat(*prompt, genAiConn, simConn, *numChats, &wg, sigCtx, chatHistory)
wg.Wait()
}

Expand All @@ -120,31 +153,68 @@ func main() {
os.Exit(0)
}

func initClient(endpoint string, context string, name string) *connection {
func initClient(endpoint string, context string, name string, npc bool, fromID int, toID int) *connection {
// TODO: create option for a client certificate
client := &http.Client{}
return &connection{client: client, endpoint: endpoint, context: context, name: name}
return &connection{client: client, endpoint: endpoint, context: context, name: name, npc: npc, fromId: fromID, toId: toID}
}

type connection struct {
client *http.Client
endpoint string // full base URL for API requests
endpoint string // Full base URL for API requests
context string
name string // human readable name for the connection
name string // Human readable name for the connection
npc bool // True if the endpoint is the NPC API
fromId int // For use with NPC API, sender ID
toId int // For use with NPC API, receiver ID
// TODO: create options for routes off the base URL
}

// For use with Vertex APIs
type GenAIRequest struct {
Context string `json:"context,omitempty"`
Prompt string `json:"prompt"`
Context string `json:"context,omitempty"` // Optional
Prompt string `json:"prompt,omitempty"`
ChatHistory []Message `json:"messages,omitempty"` // Optional, stores chat history for use with Vertex Chat API
}

func handleGenAIRequest(prompt string, clientConn *connection) (string, error) {
jsonRequest := GenAIRequest{
Context: clientConn.context,
Prompt: prompt,
// For use with NPC API
type NPCRequest struct {
Msg string `json:"message,omitempty"`
FromId int `json:"from_id,omitempty"`
ToId int `json:"to_id,omitempty"`
}

// Expected format for the NPC endpoint response
type NPCResponse struct {
Response string `json:"response"`
}

// Conversation history provided to the model in a structured alternate-author form.
// https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/text-chat
type Message struct {
Author string `json:"author"`
Content string `json:"content"`
}

func handleGenAIRequest(prompt string, clientConn *connection, chatHistory []Message) (string, error) {
var jsonStr []byte
var err error
// If the endpoint is the NPC API, use the json request format specifc to that API
if clientConn.npc {
npcRequest := NPCRequest{
Msg: prompt,
FromId: clientConn.fromId,
ToId: clientConn.toId,
}
jsonStr, err = json.Marshal(npcRequest)
} else {
genAIRequest := GenAIRequest{
Context: clientConn.context,
Prompt: prompt,
ChatHistory: chatHistory,
}
jsonStr, err = json.Marshal(genAIRequest)
}
jsonStr, err := json.Marshal(jsonRequest)
if err != nil {
return "", fmt.Errorf("unable to marshal json request: %v", err)
}
Expand Down Expand Up @@ -175,7 +245,7 @@ func handleGenAIRequest(prompt string, clientConn *connection) (string, error) {
}

// Two AIs (connection endpoints) talking to each other
func autonomousChat(prompt string, conn1 *connection, conn2 *connection, numChats int, wg *sync.WaitGroup, sigCtx context.Context) {
func autonomousChat(prompt string, conn1 *connection, conn2 *connection, numChats int, wg *sync.WaitGroup, sigCtx context.Context, chatHistory []Message) {
select {
case <-sigCtx.Done():
wg.Done()
Expand All @@ -186,15 +256,27 @@ func autonomousChat(prompt string, conn1 *connection, conn2 *connection, numChat
return
}

response, err := handleGenAIRequest(prompt, conn1)
response, err := handleGenAIRequest(prompt, conn1, chatHistory)
if err != nil {
log.Fatalf("Could not send request: %v", err)
}
// If we sent the request to the NPC endpoint we need to parse the json response {response: "response"}
if conn1.npc {
npcResponse := NPCResponse{}
err = json.Unmarshal([]byte(response), &npcResponse)
if err != nil {
log.Fatalf("Unable to unmarshal NPC endpoint response: %v", err)
}
response = npcResponse.Response
}
log.Printf("%d %s RESPONSE: %s\n", numChats, conn1.name, response)

chat := Message{Author: conn1.name, Content: response}
chatHistory = append(chatHistory, chat)

numChats -= 1
// Flip between the connection that the response is sent to.
autonomousChat(response, conn2, conn1, numChats, wg, sigCtx)
autonomousChat(response, conn2, conn1, numChats, wg, sigCtx, chatHistory)
}
}

Expand Down Expand Up @@ -225,7 +307,8 @@ func tcpHandleConnection(conn net.Conn, genAiConn *connection) {
txt := scanner.Text()
log.Printf("TCP txt: %v", txt)

response, err := handleGenAIRequest(txt, genAiConn)
// TODO: update with chathistroy
response, err := handleGenAIRequest(txt, genAiConn, nil)
if err != nil {
response = "ERROR: " + err.Error() + "\n"
}
Expand Down
Loading