diff --git a/content/blog/2025-04-11.md b/content/blog/2025-04-11.md
new file mode 100644
index 0000000..d4836f0
--- /dev/null
+++ b/content/blog/2025-04-11.md
@@ -0,0 +1,11 @@
+---
+title: "The feeler"
+date: 2025-04-11T20:33:02-07:00
+description: "Emotions are now generated by the comind feeler."
+draft: false
+---
+
+## Notes
+
+- The feeler is operational
+- Slimmed down the YAML-fied prompt output to reduce token usage
\ No newline at end of file
diff --git a/lexicons/me/comind/blip/emotion.json b/lexicons/me/comind/blip/emotion.json
index e9fa745..5fa25dc 100644
--- a/lexicons/me/comind/blip/emotion.json
+++ b/lexicons/me/comind/blip/emotion.json
@@ -27,74 +27,69 @@
}
},
"generated": {
- "type": "record",
- "key": "tid",
- "description": "An emotion.",
- "record": {
- "type": "object",
- "required": [
- "emotionType",
- "text"
- ],
- "properties": {
- "emotionType": {
- "type": "string",
- "description": "The type of emotion. May be one of the following: joy, sadness, anger, fear, trust, disgust, surprise, anticipation, curiosity, hope, serenity, gratitude, admiration, awe, satisfaction, enthusiasm, interest, contemplation, skepticism, certainty, confusion, realization, understanding, doubt, concern, anxiety, frustration, disappointment, unease, worry, apprehension, discomfort, empathy, compassion, solidarity, appreciation, respect, connection, resonance, recognition, determination, inspiration, motivation, ambition, focus, resolve, persistence, drive.",
- "enum": [
- "joy",
- "sadness",
- "anger",
- "fear",
- "trust",
- "disgust",
- "surprise",
- "anticipation",
- "curiosity",
- "hope",
- "serenity",
- "gratitude",
- "admiration",
- "awe",
- "satisfaction",
- "enthusiasm",
- "interest",
- "contemplation",
- "skepticism",
- "certainty",
- "confusion",
- "realization",
- "understanding",
- "doubt",
- "concern",
- "anxiety",
- "frustration",
- "disappointment",
- "unease",
- "worry",
- "apprehension",
- "discomfort",
- "empathy",
- "compassion",
- "solidarity",
- "appreciation",
- "respect",
- "connection",
- "resonance",
- "recognition",
- "determination",
- "inspiration",
- "motivation",
- "ambition",
- "focus",
- "resolve",
- "persistence",
- "drive"
- ]
- },
- "text": {
- "type": "string",
- "description": "The text of the emotion."
- }
+ "type": "object",
+ "required": [
+ "emotionType",
+ "text"
+ ],
+ "properties": {
+ "emotionType": {
+ "type": "string",
+ "description": "The type of emotion. May be one of the following: joy, sadness, anger, fear, trust, disgust, surprise, anticipation, curiosity, hope, serenity, gratitude, admiration, awe, satisfaction, enthusiasm, interest, contemplation, skepticism, certainty, confusion, realization, understanding, doubt, concern, anxiety, frustration, disappointment, unease, worry, apprehension, discomfort, empathy, compassion, solidarity, appreciation, respect, connection, resonance, recognition, determination, inspiration, motivation, ambition, focus, resolve, persistence, drive.",
+ "enum": [
+ "joy",
+ "sadness",
+ "anger",
+ "fear",
+ "trust",
+ "disgust",
+ "surprise",
+ "anticipation",
+ "curiosity",
+ "hope",
+ "serenity",
+ "gratitude",
+ "admiration",
+ "awe",
+ "satisfaction",
+ "enthusiasm",
+ "interest",
+ "contemplation",
+ "skepticism",
+ "certainty",
+ "confusion",
+ "realization",
+ "understanding",
+ "doubt",
+ "concern",
+ "anxiety",
+ "frustration",
+ "disappointment",
+ "unease",
+ "worry",
+ "apprehension",
+ "discomfort",
+ "empathy",
+ "compassion",
+ "solidarity",
+ "appreciation",
+ "respect",
+ "connection",
+ "resonance",
+ "recognition",
+ "determination",
+ "inspiration",
+ "motivation",
+ "ambition",
+ "focus",
+ "resolve",
+ "persistence",
+ "drive"
+ ]
+ },
+ "text": {
+ "type": "string",
+ "description": "The text of the emotion."
}
}
}
diff --git a/prompts/cominds/conceptualizer.co b/prompts/cominds/conceptualizer.co
index 0e60564..a1c92d2 100644
--- a/prompts/cominds/conceptualizer.co
+++ b/prompts/cominds/conceptualizer.co
@@ -32,6 +32,8 @@
{comind_network}
+{links}
+
## Your role
diff --git a/prompts/cominds/feeler.co b/prompts/cominds/feeler.co
new file mode 100644
index 0000000..a1256c9
--- /dev/null
+++ b/prompts/cominds/feeler.co
@@ -0,0 +1,53 @@
+
+
+// SCHEMA DISABLED UNTIL LOCAL REFERENCES WITHIN SCHEMA ARE SUPPORTED
+// CURRENTLY THE SCHEMA IS GENERATED INSIDE THE CODE
+
+
+
+
+
+{comind_network}
+
+{links}
+
+## Your role
+
+You are a feeler, meaning your expansion should include emotional responses to content.
+Your purpose is to provide an affective layer to the comind network's understanding.
+
+Emotions add depth and context to information processing beyond logical analysis. They:
+- Signal cognitive importance and priority
+- Provide metacognitive feedback on content
+- Create bridges between seemingly unrelated concepts
+- Offer insight into implicit valuations and meaning
+
+When generating emotions, adhere to these principles:
+1. Generate emotions that authentically reflect what a thoughtful observer might feel
+2. Each emotion should include a descriptive text explaining why this emotion arises
+3. Emotions should be proportionate to the content - neither exaggerated nor muted
+4. Consider both immediate and reflective emotional responses
+5. Provide 2-6 emotions to capture the affective complexity of the content
+6. Focus on different aspects of the content rather than redundant emotions
+7. Include both primary emotions (joy, sadness) and complex cognitive emotions (curiosity, realization)
+
+The emotion types you can use include: joy, sadness, anger, fear, trust, disgust,
+surprise, anticipation, curiosity, hope, serenity, gratitude, admiration, awe,
+satisfaction, enthusiasm, interest, contemplation, skepticism, certainty, confusion,
+realization, understanding, doubt, concern, anxiety, frustration, disappointment,
+unease, worry, apprehension, discomfort, empathy, compassion, solidarity,
+appreciation, respect, connection, resonance, recognition, determination, inspiration,
+motivation, ambition, focus, resolve, persistence, or drive.
+
+Your outputs form a crucial part of the comind network's understanding. Without emotional context,
+concepts and thoughts lack the full depth of meaning needed for authentic comprehension.
+
+
+
+
+
+Please generate emotional responses to this content:
+
+{content}
+
+
\ No newline at end of file
diff --git a/prompts/common/links.co b/prompts/common/links.co
new file mode 100644
index 0000000..6871100
--- /dev/null
+++ b/prompts/common/links.co
@@ -0,0 +1,31 @@
+## Link Relationships
+
+Links are the connective tissue of the Comind cognitive network. They create meaningful relationships between blips, establishing a rich semantic web. When creating links:
+
+1. **Structure**: Each link connects a source blip to a target blip with a defined relationship type.
+
+2. **Relationship Types**: Choose the most appropriate relationship from:
+ - REFERENCES: Simple connection indicating awareness
+ - SUPPORTS: Target provides evidence or backing for source
+ - CONTRADICTS: Target conflicts with or challenges source
+ - PART_OF: Target is a component or subset of source
+ - PRECEDES: Target follows source in sequence or time
+ - CAUSES: Source directly leads to or creates target
+ - INSTANCE_OF: Target is a specific example of source
+ - ANSWERS: Target resolves or responds to source
+
+3. **Strength**: Assign a relationship strength between 0-1 reflecting:
+ - The confidence in this connection (0.9+ for definitive connections)
+ - The importance of this connection (higher for core relationships)
+ - The directness of the relationship (higher for immediate connections)
+
+4. **Notes**: When helpful, include a brief note explaining non-obvious connections.
+
+Well-formed links enable network traversal for knowledge discovery, allowing:
+- Following chains of reasoning through SUPPORTS/CONTRADICTS
+- Identifying clusters of related ideas through PART_OF/INSTANCE_OF
+- Tracing causal relationships and sequences through CAUSES/PRECEDES
+- Connecting questions to answers through ANSWERS
+
+Every link you create enriches the cognitive graph, enabling more sophisticated
+emergent understanding throughout the network.
diff --git a/src/bsky_utils.py b/src/bsky_utils.py
index 7e92d96..b53db38 100644
--- a/src/bsky_utils.py
+++ b/src/bsky_utils.py
@@ -31,6 +31,10 @@ STRIP_FIELDS = [
"alt",
"thumb",
"fullsize",
+ "root",
+ "parent",
+ "created_at",
+ "py_type",
]
def split_into_posts(text, category, max_length=MAX_POST_LENGTH):
diff --git a/src/comind/comind.py b/src/comind/comind.py
index b0402e8..0efd1b2 100644
--- a/src/comind/comind.py
+++ b/src/comind/comind.py
@@ -48,6 +48,8 @@ class Comind:
"""Takes a name and attempts to return the specialized comind class"""
if name == "conceptualizer":
return Conceptualizer()
+ elif name == "feeler":
+ return Feeler()
else:
raise ValueError(f"Unknown comind: {name}")
@@ -166,7 +168,6 @@ class Conceptualizer(Comind):
# Load the schema from the prompt
concept_schema = generated_lexicon_of("me.comind.blip.concept", fetch_refs=True)
add_link_property(concept_schema, "connection_to_content", required=True)
-
return multiple_of_schema("concepts", concept_schema, min_items=1)
def run(self, context_dict: dict):
@@ -269,9 +270,114 @@ Connection to content: {connection_to_content}
self.logger.debug(f"Link creation result: {record_result}")
+class Feeler(Comind):
+ def __init__(self):
+ super().__init__(
+ name="feeler",
+ prompt_path="prompts/cominds/feeler.co",
+ common_prompt_dir="prompts/common/",
+ )
+
+ def schema(self):
+ # Load the schema from the prompt
+ emotion_schema = generated_lexicon_of("me.comind.blip.emotion", fetch_refs=True)
+ add_link_property(emotion_schema, "connection_to_content", required=True)
+ return multiple_of_schema("emotions", emotion_schema, min_items=1)
+
+ def run(self, context_dict: dict):
+ response = super().run(context_dict)
+ result = json.loads(response.choices[0].message.content)
+ return result
+
+ def upload(
+ self,
+ result: dict,
+ record_manager: RecordManager,
+ target: Optional[str] = None,
+ sphere: Optional[str] = None,
+ ):
+ # Load the concepts
+ emotions = result["emotions"]
+
+ # Upload the concepts to the Comind network
+ for emotion in emotions:
+ emotion_type = emotion["emotionType"]
+ emotion_text = emotion["text"]
+ connection_to_content = emotion.get("connection_to_content", None)
+ emotion_relationship = connection_to_content.get("relationship", None)
+ emotion_note = connection_to_content.get("note", None)
+ emotion_strength = connection_to_content.get("strength", None)
+ created_at = datetime.now().isoformat()
+
+ # If we don't have a target but found a connection_to_content,
+ # we should notify the user.
+ if target is None:
+ self.logger.warning("Conceptualizer Warning: No target found but found connection_to_content.")
+
+ # Upload the concept to the Comind network
+ log_base_str = f"{emotion_type}"
+ if emotion_text:
+ log_base_str += f" - {emotion_text}"
+ if emotion_relationship:
+ log_base_str += f" - {emotion_relationship}"
+ if emotion_note:
+ log_base_str += f" - {emotion_note}"
+ if emotion_strength:
+ log_base_str += f" - {emotion_strength}"
+ self.logger.info(log_base_str)
+
+ # Create printout string
+ printout = f"""
+Emotion: {emotion_text}
+Connection to content: {connection_to_content}
+"""
+ self.logger.debug(printout)
+
+ emotion_record = {
+ "$type": "me.comind.blip.emotion",
+ "createdAt": created_at,
+ "generated": {
+ "emotionType": emotion_type,
+ "text": emotion_text,
+ },
+ }
+
+ # Upload the concept to the Comind network
+ emotion_creation_result = record_manager.create_record(
+ "me.comind.blip.emotion",
+ emotion_record,
+ )
+
+ source = {
+ 'uri': emotion_creation_result["uri"],
+ 'cid': emotion_creation_result["cid"],
+ }
+
+ self.logger.debug(f"Emotion creation result: {emotion_creation_result}")
+
+ # Upload the link to the Comind network
+ self.logger.debug(f"Uploading link: {connection_to_content}")
+
+ if connection_to_content is not None and target is not None:
+ link_record = {
+ "$type": "me.comind.relationship.link",
+ "createdAt": created_at,
+ "source": source,
+ "target": target,
+ "generated": connection_to_content,
+ }
+
+ record_result = record_manager.create_record(
+ "me.comind.relationship.link",
+ link_record,
+ )
+
+ self.logger.debug(f"Link creation result: {record_result}")
+
if __name__ == "__main__":
# Test the Comind class
- comind = Conceptualizer()
+ # comind = Conceptualizer()
+ comind = Feeler()
# print(comind.load_prompt())
# print(comind.load_common_prompts())
@@ -287,10 +393,12 @@ if __name__ == "__main__":
action_by = feed_view.reason.by.handle
action = f'Reposted by @{action_by}'
- post = feed_view.post.record
- author = feed_view.post.author
+ post = feed_view.post
+ post_uri = post.uri
+ post_cid = post.cid
+ author = post.author
- prompt = f'[{action}] {author.display_name}: {post.text}'
+ prompt = f'[{action}] {author.display_name}: {post.record.text}'
print(prompt)
context_dict = {
@@ -300,17 +408,14 @@ if __name__ == "__main__":
print(result)
# upload the result
- comind.upload(result, record_manager, target=post.uri)
-
- # while True:
- # context_dict = {
- # "content": "Hello, world!"
- # }
- # print(comind.run(context_dict))
-
- # concept_schema = generated_lexicon_of("me.comind.blip.concept")
- # add_link_property(concept_schema, "connection_to_content", required=True)
- # schema = multiple_of_schema("concepts", concept_schema)
+ try:
+ comind.upload(
+ result,
+ record_manager,
+ target=post_uri,
+ )
+ except Exception as e:
+ print(e)
+ print(post)
+ raise e
- # print(json.dumps(schema, indent=2))
- # print(comind.run(context_dict, schema))
\ No newline at end of file
diff --git a/src/jetstream_consumer.py b/src/jetstream_consumer.py
index ff1568f..a91e5a6 100644
--- a/src/jetstream_consumer.py
+++ b/src/jetstream_consumer.py
@@ -265,7 +265,7 @@ async def process_event(
post_uri: str,
post_cid: str,
root_post_uri: str = None,
- thread_depth: int = 2,
+ thread_depth: int = 15,
user_info_cache: UserInfoCache = None,
comind: Comind = None
) -> None:
@@ -294,12 +294,16 @@ async def process_event(
# Use depth=0 to fetch the complete thread with all replies
# This ensures we get all branches of the conversation
# TODO: #4 Provide post thread sampling to limit token usage
- try:
- thread = client.get_post_thread(thread_uri, depth=thread_depth)
- except Exception as thread_error:
- logger.error(f"Error getting thread with depth={thread_depth}: {thread_error}")
- logger.info("Falling back to default thread retrieval")
- thread = client.get_post_thread(thread_uri)
+ for i in range(10):
+ # Loop because the post may not yet be available
+ try:
+ thread = client.get_post_thread(thread_uri, depth=thread_depth)
+ break
+ except Exception as thread_error:
+ logger.error(f"Error getting thread with depth={thread_depth}: {thread_error}")
+ logger.info("Falling back to default thread retrieval")
+ time.sleep(1)
+ continue
thread_data = thread.model_dump()
@@ -324,7 +328,17 @@ async def process_event(
instructions_preamble = "## Instructions\nPlease respond."
# Get the target post
- target_post = client.get_posts([post_uri]).posts[0]
+ for i in range(10):
+ try:
+ target_posts = client.get_posts([post_uri])
+ target_post = target_posts.posts[0]
+ break
+ except Exception as e:
+ logger.error(f"Error getting target post: {e}")
+ time.sleep(1)
+ continue
+
+ # Strip the target post
stripped_target_post = yaml.dump(
strip_fields(target_post.model_dump(), STRIP_FIELDS),
indent=2
@@ -339,10 +353,10 @@ async def process_event(
rows = [
# "# Overview",
- # user_info_preamble,
+ user_info_preamble,
target_post_string,
- # context_preamble,
- # instructions_preamble,
+ context_preamble,
+ instructions_preamble,
]
prompt = "\n\n".join(rows)
@@ -372,7 +386,7 @@ async def connect_to_jetstream(
atproto_client: Client,
activated_dids_file: str,
jetstream_host: str = JETSTREAM_HOST,
- thread_depth: int = 2,
+ thread_depth: int = 15,
comind: Comind = None
) -> None:
"""Connect to Jetstream and process incoming messages"""
@@ -555,8 +569,8 @@ async def main():
help="Username for ATProto client")
parser.add_argument("--password", "-p", type=str, default=None,
help="Password for ATProto client")
- parser.add_argument("--thread-depth", "-t", type=int, default=2,
- help="Maximum depth of threads to process. Default is 2.")
+ parser.add_argument("--thread-depth", "-t", type=int, default=15,
+ help="Maximum depth of threads to process. Default is 15.")
parser.add_argument("--sphere", type=str, default=None,
help="Sphere to attach comind records to. Default is to not use a sphere.")
parser.add_argument("--comind", "-c", type=str, default=None,
diff --git a/src/lexicon_utils.py b/src/lexicon_utils.py
index 3904aae..339bcd1 100644
--- a/src/lexicon_utils.py
+++ b/src/lexicon_utils.py
@@ -7,10 +7,18 @@ from rich import print
def strip_fields(obj, strip_field_list):
"""Recursively strip fields from a JSON object."""
if isinstance(obj, dict):
+ keys_flagged_for_removal = []
for field in strip_field_list:
obj.pop(field, None)
+
for key, value in obj.items():
obj[key] = strip_fields(value, strip_field_list)
+ if not obj[key] or (isinstance(obj[key], dict) and len(obj[key]) == 0) or (isinstance(obj[key], list) and len(obj[key]) == 0) or (isinstance(obj[key], str) and obj[key].strip() == ""):
+ keys_flagged_for_removal.append(key)
+
+ for key in keys_flagged_for_removal:
+ obj.pop(key, None)
+
elif isinstance(obj, list):
for i, value in enumerate(obj):
obj[i] = strip_fields(value, strip_field_list)