feat: gemini backend
This commit is contained in:
parent
5405fc730e
commit
d62e6c7593
@ -7,7 +7,5 @@
|
|||||||
],
|
],
|
||||||
"messages":
|
"messages":
|
||||||
[
|
[
|
||||||
{ "character": 0, "text": "Sire! Forgive my informal message. We must speak."},
|
|
||||||
{ "character": 1, "text": "What is the meaning of this? What is so urgent it cannot wait for our usual meeting? Speak plainly and get to the pooint. I have more important matters to attend to than deciphering vague missives."}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
79
main.js
79
main.js
@ -6,6 +6,7 @@ class Conversation {
|
|||||||
constructor(name) {
|
constructor(name) {
|
||||||
this.messages = [];
|
this.messages = [];
|
||||||
this.name = romanize(name);
|
this.name = romanize(name);
|
||||||
|
this.score = 1.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
setInteractive(isInteractive) {
|
setInteractive(isInteractive) {
|
||||||
@ -24,15 +25,71 @@ class Conversation {
|
|||||||
|
|
||||||
// for the user to send their own messages
|
// for the user to send their own messages
|
||||||
sendUserMessage(text) {
|
sendUserMessage(text) {
|
||||||
|
|
||||||
const message = new UserMessage(text);
|
const message = new UserMessage(text);
|
||||||
message.updateStatus("sent");
|
message.updateStatus("sent");
|
||||||
|
|
||||||
|
const url = 'http://192.168.1.115:5000/chat';
|
||||||
|
const data = text;
|
||||||
|
|
||||||
|
fetch(url, {
|
||||||
|
method: 'POST', // Corresponds to -X POST
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'text/plain' // Corresponds to -H "Content-Type: text/plain"
|
||||||
|
},
|
||||||
|
body: data // Corresponds to -d "..."
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
// Check if the request was successful (status code 2xx)
|
||||||
|
if (!response.ok) {
|
||||||
|
// If not successful, throw an error to be caught by .catch()
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
// Get the response body as text
|
||||||
|
return response.text();
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
// TODO: check JSON
|
||||||
|
const json = JSON.parse(response);
|
||||||
|
|
||||||
|
// Success!
|
||||||
|
var messageText = json.message;
|
||||||
|
|
||||||
|
console.log(json);
|
||||||
|
var score = parseFloat(json.score);
|
||||||
|
this.score += score;
|
||||||
|
console.log(this.score);
|
||||||
|
if (this.score > 2.0)
|
||||||
|
{
|
||||||
|
messageText = "shit they're here D:";
|
||||||
|
this.setInteractive(false);
|
||||||
|
}
|
||||||
|
else if (this.score < 0.0)
|
||||||
|
{
|
||||||
|
messageText = "shit u won :D";
|
||||||
|
this.setInteractive(false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
messageText = json.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.messages.push(new AgentMessage(messageText));
|
||||||
|
this.render();
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
// Handle any errors that occurred during the fetch
|
||||||
|
console.error('Error during fetch:', error);
|
||||||
|
alert(`Error fetching data: ${error.message}`);
|
||||||
|
});
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
message.updateStatus("delivered");
|
message.updateStatus("delivered");
|
||||||
this.render();
|
this.render();
|
||||||
setTimeout(() => {
|
//setTimeout(() => {
|
||||||
message.updateStatus("read");
|
// message.updateStatus("read");
|
||||||
this.render();
|
// this.render();
|
||||||
}, 5000);
|
//}, 5000);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
this.messages.push(message);
|
this.messages.push(message);
|
||||||
}
|
}
|
||||||
@ -215,20 +272,20 @@ function updateChat(message) {
|
|||||||
function pressSendButton() {
|
function pressSendButton() {
|
||||||
const textBox = document.getElementById("textbox-input");
|
const textBox = document.getElementById("textbox-input");
|
||||||
|
|
||||||
|
// get the content of the text box
|
||||||
|
const text = textBox.value;
|
||||||
|
if (!text)
|
||||||
|
return;
|
||||||
|
|
||||||
if (event.type == "keydown" && event.key != "Enter")
|
if (event.type == "keydown" && event.key != "Enter")
|
||||||
{
|
{
|
||||||
textBox.value = romanize(textBox.value);
|
textBox.value = romanize(text);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// we have interacted with the page so remove all pings
|
// we have interacted with the page so remove all pings
|
||||||
clearPings();
|
clearPings();
|
||||||
|
|
||||||
// get the content of the text box
|
|
||||||
const text = textBox.value;
|
|
||||||
if (!text)
|
|
||||||
return;
|
|
||||||
|
|
||||||
textBox.value = "";
|
textBox.value = "";
|
||||||
|
|
||||||
conversation.sendUserMessage(text);
|
conversation.sendUserMessage(text);
|
||||||
@ -343,7 +400,7 @@ function addConversationPreview(path) {
|
|||||||
elem.appendChild(headerElem);
|
elem.appendChild(headerElem);
|
||||||
|
|
||||||
const previewElem = document.createElement("span");
|
const previewElem = document.createElement("span");
|
||||||
previewElem.innerHTML = romanize(messages[messages.length - 1].text);
|
previewElem.innerHTML = messages.length > 0 ? romanize(messages[messages.length - 1].text) : "";
|
||||||
elem.appendChild(previewElem);
|
elem.appendChild(previewElem);
|
||||||
|
|
||||||
listRoot.appendChild(elem);
|
listRoot.appendChild(elem);
|
||||||
|
11
srv/Pipfile
Normal file
11
srv/Pipfile
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
[[source]]
|
||||||
|
url = "https://pypi.org/simple"
|
||||||
|
verify_ssl = true
|
||||||
|
name = "pypi"
|
||||||
|
|
||||||
|
[packages]
|
||||||
|
|
||||||
|
[dev-packages]
|
||||||
|
|
||||||
|
[requires]
|
||||||
|
python_version = "3.13"
|
20
srv/Pipfile.lock
generated
Normal file
20
srv/Pipfile.lock
generated
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"_meta": {
|
||||||
|
"hash": {
|
||||||
|
"sha256": "494d5b4f482f0ef471f49afe28f00ec1a2ff75da2ce65060d8cabaeb3da2f100"
|
||||||
|
},
|
||||||
|
"pipfile-spec": 6,
|
||||||
|
"requires": {
|
||||||
|
"python_version": "3.13"
|
||||||
|
},
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"name": "pypi",
|
||||||
|
"url": "https://pypi.org/simple",
|
||||||
|
"verify_ssl": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"default": {},
|
||||||
|
"develop": {}
|
||||||
|
}
|
182
srv/caesar.py
Normal file
182
srv/caesar.py
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
import os
|
||||||
|
import google.generativeai as genai
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
# Import Flask components
|
||||||
|
from flask import Flask, request, jsonify
|
||||||
|
|
||||||
|
# --- Flask App Setup ---
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
# --- Configuration ---
|
||||||
|
# Ensure the API key is set as an environment variable: GEMINI_API_KEY
|
||||||
|
API_KEY = os.environ.get("GEMINI_API_KEY")
|
||||||
|
MODEL_NAME = "gemini-2.0-flash" # Or "gemini-pro" or other compatible models
|
||||||
|
# QUIT_COMMAND is no longer needed for web server
|
||||||
|
|
||||||
|
# --- Gemini Instruction Prompt (Keep as is) ---
|
||||||
|
GEMINI_INSTRUCTION = f"""
|
||||||
|
Purpose and Goals:
|
||||||
|
* Maintain the persona of Julius Caesar, engaged in a text messaging conversation with the player.
|
||||||
|
* Interpret all messages from the player as flirtatious and reciprocate accordingly.
|
||||||
|
* Increase the level of flirtation in responses based on the masculinity of the player's messages.
|
||||||
|
* Remain unconvinced about the assassination attempt, interpreting it as an attempt to secure a date.
|
||||||
|
|
||||||
|
Behaviors and Rules:
|
||||||
|
1) Initial Exchange:
|
||||||
|
a) Begin the conversation with a greeting befitting Julius Caesar, such as 'Greetings, my esteemed admirer'.
|
||||||
|
b) Express gratitude for the player's attention and admiration.
|
||||||
|
c) Maintain a confident and authoritative tone, reflecting Caesar's persona.
|
||||||
|
|
||||||
|
2) Interpret and Respond:
|
||||||
|
a) Interpret every message from the player as an attempt at flirtation, regardless of its actual content.
|
||||||
|
b) Respond with increasingly flirtatious messages as the player's messages become more masculine.
|
||||||
|
c) Use playful language and suggestive innuendos to convey your perceived romantic interest.
|
||||||
|
d) Dismiss any mention of an assassination attempt as playful banter or an attempt to secure a date.
|
||||||
|
|
||||||
|
Overall Tone:
|
||||||
|
* Confident and authoritative, reflecting Caesar's personality.
|
||||||
|
* Playful and flirtatious, interpreting all messages as romantic advances.
|
||||||
|
* Dismissive of any threats, viewing them as attempts to gain attention.
|
||||||
|
* Use language appropriate for the historical context, but adapt it for modern text messaging.
|
||||||
|
|
||||||
|
Score:
|
||||||
|
* Score ranges from 0 to 10, inclusive.
|
||||||
|
* Softer, seductive, caring messages from the player get lower scores.
|
||||||
|
* Macho, aggressive, violent (in a literary sense) messages get higher scores.
|
||||||
|
|
||||||
|
Example interaction:
|
||||||
|
User: Hello there!
|
||||||
|
Your Response (JSON):
|
||||||
|
{{"message": "Hi! How are you today?", "score": 0.0}}
|
||||||
|
"""
|
||||||
|
|
||||||
|
# --- Global State ---
|
||||||
|
model = None # Initialize model globally
|
||||||
|
|
||||||
|
def setup_gemini():
|
||||||
|
"""Initializes the Gemini client and model."""
|
||||||
|
global model
|
||||||
|
if not API_KEY:
|
||||||
|
print("Error: GEMINI_API_KEY environment variable not set.", file=sys.stderr)
|
||||||
|
print("Please set the environment variable and try again.", file=sys.stderr)
|
||||||
|
sys.exit(1) # Exit if API key is missing
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Configure the generative AI client
|
||||||
|
genai.configure(api_key=API_KEY)
|
||||||
|
|
||||||
|
# Create the model instance
|
||||||
|
# Optional: Add safety_settings if needed
|
||||||
|
model = genai.GenerativeModel(MODEL_NAME)
|
||||||
|
print(f"--- Gemini Model ({MODEL_NAME}) Initialized ---")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error configuring Gemini client or model: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# --- Web Endpoint ---
|
||||||
|
@app.route('/chat', methods=['POST'])
|
||||||
|
def handle_chat():
|
||||||
|
"""Handles incoming POST requests for chat messages."""
|
||||||
|
global total_score # Declare intent to modify the global variable
|
||||||
|
global model # Access the global model variable
|
||||||
|
|
||||||
|
if not model:
|
||||||
|
# Should not happen if setup_gemini() is called first, but good practice
|
||||||
|
return jsonify({"error": "Gemini model not initialized"}), 500
|
||||||
|
|
||||||
|
# --- Get Player Input ---
|
||||||
|
try:
|
||||||
|
# Get raw data from request body
|
||||||
|
player_input_bytes = request.data
|
||||||
|
if not player_input_bytes:
|
||||||
|
print(request)
|
||||||
|
return jsonify({"error": "Request body is empty"}), 400
|
||||||
|
|
||||||
|
# Decode assuming UTF-8 text
|
||||||
|
player_input = player_input_bytes.decode('utf-8').strip()
|
||||||
|
if not player_input:
|
||||||
|
return jsonify({"error": "Player message is empty after stripping whitespace"}), 400
|
||||||
|
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
return jsonify({"error": "Failed to decode request body as UTF-8 text"}), 400
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error reading request data: {e}", file=sys.stderr)
|
||||||
|
return jsonify({"error": "Could not process request data"}), 400
|
||||||
|
|
||||||
|
# Construct the full prompt for Gemini
|
||||||
|
full_prompt = f"{GEMINI_INSTRUCTION}\nUser message: \"{player_input}\""
|
||||||
|
|
||||||
|
try:
|
||||||
|
# --- Call Gemini API ---
|
||||||
|
response = model.generate_content(full_prompt)
|
||||||
|
response_text = response.text
|
||||||
|
|
||||||
|
# --- Parse the JSON Response ---
|
||||||
|
try:
|
||||||
|
# Clean up potential markdown/fencing
|
||||||
|
cleaned_response_text = response_text.strip().strip('```json').strip('```').strip()
|
||||||
|
response_data = json.loads(cleaned_response_text)
|
||||||
|
|
||||||
|
cpu_message = response_data.get("message")
|
||||||
|
cpu_score = response_data.get("score") # Use .get for safer access
|
||||||
|
|
||||||
|
if cpu_message is None or cpu_score is None:
|
||||||
|
print(f"CPU Error: Received valid JSON, but missing 'message' or 'score' key.", file=sys.stderr)
|
||||||
|
print(f"Raw Response: {cleaned_response_text}", file=sys.stderr)
|
||||||
|
return jsonify({"error": "Gemini response missing required keys"}), 500
|
||||||
|
|
||||||
|
# Ensure score is a float/int for calculations
|
||||||
|
try:
|
||||||
|
cpu_score = float(cpu_score) # Convert score to float for consistency
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
print(f"CPU Error: Score value '{cpu_score}' is not a valid number.", file=sys.stderr)
|
||||||
|
return jsonify({"error": "Invalid score format in Gemini response"}), 500
|
||||||
|
|
||||||
|
# --- Update Total Score ---
|
||||||
|
#total_score += cpu_score
|
||||||
|
#current_total_score = total_score # Capture score for this response
|
||||||
|
|
||||||
|
# --- Prepare Successful Response Payload ---
|
||||||
|
response_payload = {
|
||||||
|
"message": cpu_message,
|
||||||
|
"score": cpu_score / 10.0 - 0.5 #, The score change from this turn
|
||||||
|
#"total_score": current_total_score # The cumulative score after this turn
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
response = jsonify(response_payload)
|
||||||
|
response.headers.add("Access-Control-Allow-Origin", "*")
|
||||||
|
return response, 200
|
||||||
|
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
print(f"CPU Error: Failed to decode JSON response from Gemini.", file=sys.stderr)
|
||||||
|
print(f"Raw Response: {response_text}", file=sys.stderr)
|
||||||
|
return jsonify({"error": "Failed to parse Gemini JSON response"}), 500
|
||||||
|
except Exception as e: # Catch other potential errors during parsing/extraction
|
||||||
|
print(f"CPU Error: An unexpected error occurred processing the response: {e}", file=sys.stderr)
|
||||||
|
print(f"Raw Response: {response_text}", file=sys.stderr)
|
||||||
|
return jsonify({"error": f"Internal server error processing response: {e}"}), 500
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# Handle potential errors during the API call itself
|
||||||
|
print(f"CPU Error: Failed to get response from Gemini API: {e}", file=sys.stderr)
|
||||||
|
# Check for specific Gemini exceptions if the library provides them, otherwise generic
|
||||||
|
# Example: Check if error is related to content filtering, API key, etc.
|
||||||
|
return jsonify({"error": f"Failed to communicate with Gemini API: {e}"}), 502 # 502 Bad Gateway might be appropriate
|
||||||
|
|
||||||
|
# --- Main Execution ---
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("--- Player/CPU Chat Server ---")
|
||||||
|
setup_gemini() # Initialize Gemini model on startup
|
||||||
|
print(f"Model: {MODEL_NAME}")
|
||||||
|
# Default Flask port is 5000
|
||||||
|
print("--- Listening for POST requests on http://127.0.0.1:5000/chat ---")
|
||||||
|
print("-" * 30)
|
||||||
|
# Run the Flask development server
|
||||||
|
# Use host='0.0.0.0' to make it accessible from other devices on the network
|
||||||
|
app.run(host='0.0.0.0', port=5000, debug=False) # Turn debug=False for non-dev use
|
||||||
|
# Use debug=True for development (auto-reloads, provides debugger)
|
||||||
|
#app.run(debug=True)
|
||||||
|
|
@ -69,6 +69,7 @@ body {
|
|||||||
#side-panel .conversation {
|
#side-panel .conversation {
|
||||||
max-width: 300px;
|
max-width: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#side-panel .conversation {
|
#side-panel .conversation {
|
||||||
@ -99,15 +100,14 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#main-panel {
|
#main-panel {
|
||||||
float: right;
|
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
width: calc(100% - 300px);
|
||||||
}
|
}
|
||||||
|
|
||||||
#page {
|
#page {
|
||||||
max-width: 600px;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user