Improving an "artificial personality" using OpenAI APIs
Switching to a memory graph and creating personality from an epub
I’ve worked further on my “artificial personality” by adding a new way to bring in long term memories. I’ve also created the ability to generate a personality json file and memory files from a character in a novel. Both were a bit painful to implement which I will detail below.
All code is available at my github here.
Memory Graphs
At present long term memories are pulled by tags. Every interaction that is made is tagged by a separate call to the model. Tags are checked against existing tags in long term memory and memories that match are returned to the max long term memory limit (which is 15 long term memories at present). The interaction is then saved as a memory itself with the tags previously generated. This works well enough but it isn’t perfect and I wanted to better mimic how people really remember thinks, well how I think they do anyway. To do this I implemented a memory graph. This plots memories on a graph by tag, recency, and emotional context. Recency is generated by time stamps, emotional context by a separate function. With these plots similar memories can be grouped up and when a new interaction is made and plotted on the graph those closest memories to a certain radius can be drawn in.
To implement this I used a python package called networkx, there are others that can do this but this seemed the simplest to implement. I also needed to convert existing memories to a graph. This can be seen in the script below:
import json
import networkx as nx
from datetime import datetime
import numpy as np
from collections import defaultdict
def load_memories(file_path):
"""Load memories from JSON file."""
with open(file_path, 'r') as f:
return json.load(f)
def calculate_tag_similarity(tags1, tags2):
"""Calculate Jaccard similarity between two sets of tags."""
if not tags1 or not tags2:
return 0
set1 = set(tags1)
set2 = set(tags2)
intersection = len(set1.intersection(set2))
union = len(set1.union(set2))
return intersection / union if union > 0 else 0
def calculate_temporal_proximity(time1, time2, max_time_diff=86400): # 24 hours in seconds
"""Calculate temporal proximity between two timestamps."""
t1 = datetime.fromisoformat(time1)
t2 = datetime.fromisoformat(time2)
time_diff = abs((t1 - t2).total_seconds())
return max(0, 1 - (time_diff / max_time_diff))
def create_memory_graph(memories):
"""Create a NetworkX graph from memories."""
G = nx.Graph()
# Create nodes
for idx, memory in enumerate(memories):
node_id = idx
G.add_node(node_id,
message=memory['user_message'],
response=memory['bot_response'],
timestamp=memory['timestamp'],
tags=memory['tags'],
priority=memory.get('priority_score', 0.5))
# Create edges based on tag similarity and temporal proximity
for i in range(len(memories)):
for j in range(i + 1, len(memories)):
# Calculate tag similarity
tag_sim = calculate_tag_similarity(memories[i]['tags'], memories[j]['tags'])
# Calculate temporal proximity
temp_prox = calculate_temporal_proximity(memories[i]['timestamp'],
memories[j]['timestamp'])
# Combine scores with weights
edge_weight = (0.7 * tag_sim) + (0.3 * temp_prox)
# Add edge if weight is significant
if edge_weight > 0.1: # Threshold to avoid too many weak connections
G.add_edge(i, j, weight=edge_weight)
return G
def analyze_graph(G):
"""Analyze the graph structure."""
analysis = {
'num_nodes': G.number_of_nodes(),
'num_edges': G.number_of_edges(),
'density': nx.density(G),
'avg_clustering': nx.average_clustering(G),
'communities': len(list(nx.community.greedy_modularity_communities(G))),
}
return analysis
def find_similar_memories(G, query_tags, top_n=5):
"""Find most similar memories to a query based on tags."""
similarities = []
# Calculate similarity for each node
for node in G.nodes():
node_tags = G.nodes[node]['tags']
sim = calculate_tag_similarity(query_tags, node_tags)
similarities.append((node, sim))
# Sort by similarity and return top N
return sorted(similarities, key=lambda x: x[1], reverse=True)[:top_n]
def save_graph(G, file_path):
"""Save graph to JSON format."""
graph_data = {
'nodes': [[n, data] for n, data in G.nodes(data=True)],
'edges': [[u, v, data] for u, v, data in G.edges(data=True)]
}
with open(file_path, 'w') as f:
json.dump(graph_data, f, indent=4)
def main():
# Load memories
memories = load_memories('long_memory.json')
# Create graph
G = create_memory_graph(memories)
# Analyze graph
analysis = analyze_graph(G)
print("\nGraph Analysis:")
for key, value in analysis.items():
print(f"{key}: {value}")
# Save graph
save_graph(G, 'memory_graph.json')
print("\nGraph saved to memory_graph.json")
# Example of finding similar memories
example_query = ["coding", "memory", "improvement"]
similar_memories = find_similar_memories(G, example_query)
print("\nExample Query Results:")
for node_id, similarity in similar_memories:
print(f"Memory {node_id}: Similarity {similarity:.2f}")
print(f"Message: {G.nodes[node_id]['message'][:100]}...")
if __name__ == "__main__":
main()With that done, now we need to implement a Class for the graph in the main function itself. This is best seen in my github but basically it integrates by creating an alternative way of pulling long term memories with tags only as a fall back.
Results of the new memory graph
Honestly it is hard to tell the impact yet. I have been chatting to the personality I created a fair bit, now at over 250 interactions and I do find it much better to talk to. One interesting interaction is I mentioned I was getting a coffee and going to write something, then a couple hours later I came back and loaded up the script and the bot asked how my coffee was and how the writing went. When I had bugs it mentioned bugs I had been dealing with the day before. Memory is working so now its a matter of running tests on the memory to see how it performs over time and with the graph.
Making a new personality from a character in a novel
My next challenge was to create a personality from a novel by providing an epub file and a character name. This wasn’t too hard as I already had code written, which I may detail here later, which can read an epub and summarise the novel in a synopsis, create a setting, or generate details of a specific character. It didn’t take much to modify the code for this use case. The hardest part was making the prompt right to generate the personality in the depth I wanted, and to generate the memories. First try at the personality was far too limited for usage, the prompt returned a small JSON with basic character traits. Changing the prompt up requesting more detail fixed this.
Memories were the main challenge. Creating memories by ingesting the whole epub generated a whole five memories. Format was fine but five memories of a characters past is nowhere near sufficient. To fix this I chunked the book into chunks of 2000 tokens using tiktoken. I then processed every chunk in a separate call to the model to get memories. This took about ten minutes and I ended up with over 650 memories. This was probably overkill but it worked and I can always change the token size later.
Testing a generated personality from a character
Trying out the generated personality was interesting. I used Carl from Dungeon Crawler Carl, a book I enjoyed earlier this year. The sarcastic nature certainly came through and I did also get references to Princess Donut, his cat. However, the memory graph did struggle to work because some of the memories were saved with timestamps in the future. An odd bug but a script to correct this was simple enough. Luckily also the generated personality still worked regardless as the backup tagging system still ran.
Next steps
Next I will look to build out an artificial personality from text messages, or maybe twitter messages. Whatever I can get my hands on really. Will see how it goes. Also I need to test the temporal and emotional awareness of what is there at present. I also want to plot the graphs of interactions in a graphical way to test this too. Will update here in a couple of days.

