diff --git a/content/docs/guide/prompts.md b/content/docs/guide/prompts.md
new file mode 100644
index 0000000..b01de37
--- /dev/null
+++ b/content/docs/guide/prompts.md
@@ -0,0 +1,9 @@
+---
+title: "Prompts"
+date: 2025-04-27T18:33:00-07:00
+draft: true
+---
+
+# Best practices for prompts
+
+- Use "core perspective" instead of "core directive", to permit a less forceful tone. We work together to create a shared perspective.
diff --git a/docker-compose.yml b/docker-compose.yml
index eb11c4e..366a616 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -18,7 +18,7 @@ services:
ports:
- "8002:8000"
command: >
- --model microsoft/Phi-3.5-mini-instruct
+ --model microsoft/Phi-4
--max_model_len 15000
--guided-decoding-backend outlines
diff --git a/modal_client.py b/modal_client.py
new file mode 100644
index 0000000..91d0ac4
--- /dev/null
+++ b/modal_client.py
@@ -0,0 +1,93 @@
+"""
+Client example for connecting to the Comind Modal inference server.
+
+This script demonstrates how to use the OpenAI client library to connect
+to the Modal-hosted vLLM server.
+
+Usage:
+ python modal_client.py --prompt "Your prompt here"
+"""
+
+import argparse
+from openai import OpenAI
+
+# ANSI colors for prettier output
+BLUE = "\033[94m"
+GREEN = "\033[92m"
+RED = "\033[91m"
+BOLD = "\033[1m"
+END = "\033[0m"
+
+def main():
+ parser = argparse.ArgumentParser(description="Comind Modal LLM Client")
+ parser.add_argument("--prompt", type=str, default="Hello! How are you today?",
+ help="The prompt to send to the LLM")
+ parser.add_argument("--workspace", type=str, required=True,
+ help="Your Modal workspace name")
+ parser.add_argument("--api-key", type=str, default="comind-api-key",
+ help="API key matching the one in modal_inference.py")
+ parser.add_argument("--model", type=str, default="phi4",
+ help="Model endpoint to use (phi4 or embeddings)")
+ parser.add_argument("--stream", action="store_true",
+ help="Whether to stream the response")
+
+ args = parser.parse_args()
+
+ # Construct the base URL based on the model choice
+ if args.model == "phi4":
+ function_name = "serve-phi4"
+ model_name = "microsoft/Phi-4"
+ elif args.model == "embeddings":
+ function_name = "embeddings"
+ model_name = "mixedbread-ai/mxbai-embed-xsmall-v1"
+ else:
+ print(f"{RED}Error: Unknown model '{args.model}'{END}")
+ return
+
+ base_url = f"https://{args.workspace}--comind-vllm-inference-{function_name}.modal.run/v1"
+
+ # Initialize the OpenAI client with our Modal API endpoint
+ client = OpenAI(
+ api_key=args.api_key,
+ base_url=base_url
+ )
+
+ print(f"{BOLD}Connecting to:{END} {base_url}")
+ print(f"{BOLD}Prompt:{END} {args.prompt}")
+
+ try:
+ # Create messages for the chat API
+ messages = [{"role": "user", "content": args.prompt}]
+
+ if args.stream:
+ # Stream the response for a more interactive experience
+ print(f"\n{BOLD}{GREEN}Response:{END}", end=" ")
+ response = client.chat.completions.create(
+ model=model_name,
+ messages=messages,
+ stream=True
+ )
+
+ for chunk in response:
+ content = chunk.choices[0].delta.content
+ if content:
+ print(f"{content}", end="", flush=True)
+ print("\n")
+ else:
+ # Get the full response at once
+ response = client.chat.completions.create(
+ model=model_name,
+ messages=messages
+ )
+ print(f"\n{BOLD}{GREEN}Response:{END} {response.choices[0].message.content}\n")
+
+ except Exception as e:
+ print(f"\n{RED}Error: {str(e)}{END}")
+ print(f"\n{BLUE}Troubleshooting:{END}")
+ print(" - Check that your Modal server is running")
+ print(" - Verify the workspace name is correct")
+ print(" - Ensure the API key matches the one in modal_inference.py")
+ print(" - Check that you're using the correct model endpoint")
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/modal_inference.py b/modal_inference.py
new file mode 100644
index 0000000..a7009a7
--- /dev/null
+++ b/modal_inference.py
@@ -0,0 +1,231 @@
+import modal
+
+# Set up the container image with vLLM and necessary packages
+vllm_image = (
+ modal.Image.debian_slim(python_version="3.12")
+ .pip_install(
+ "vllm==0.7.2",
+ "huggingface_hub[hf_transfer]==0.26.2",
+ "flashinfer-python==0.2.0.post2", # pinning, very unstable
+ extra_index_url="https://flashinfer.ai/whl/cu124/torch2.5",
+ )
+ .env({"HF_HUB_ENABLE_HF_TRANSFER": "1"}) # faster model transfers
+)
+
+# Enable vLLM's V1 engine for better performance
+vllm_image = vllm_image.env({"VLLM_USE_V1": "1"})
+
+# Set up persistent volumes to cache models between runs
+hf_cache_vol = modal.Volume.from_name("huggingface-cache", create_if_missing=True)
+vllm_cache_vol = modal.Volume.from_name("vllm-cache", create_if_missing=True)
+
+# Create our Modal application
+app = modal.App("comind-vllm-inference")
+
+# Configuration options (can be modified as needed)
+MINUTES = 60 # seconds
+VLLM_PORT = 8000
+API_KEY = "comind-api-key" # Replace with a secret for production use
+
+# Define available models (uncomment desired model)
+MODELS = {
+ "phi4": {
+ "name": "microsoft/Phi-4",
+ "revision": None, # Use latest
+ "gpu": "A100:1", # Adjust based on model size and budget
+ },
+ # "llama3": {
+ # "name": "neuralmagic/Meta-Llama-3.1-8B-Instruct-quantized.w4a16",
+ # "revision": "a7c09948d9a632c2c840722f519672cd94af885d",
+ # "gpu": "A10G:1",
+ # },
+ # Uncomment and add other models as needed
+}
+
+@app.function(
+ image=vllm_image,
+ volumes={
+ "/root/.cache/huggingface": hf_cache_vol,
+ "/root/.cache/vllm": vllm_cache_vol,
+ },
+)
+@modal.web_server(port=VLLM_PORT, startup_timeout=5 * MINUTES)
+def serve_model(model_key="phi4"):
+ """
+ Serves a vLLM model with OpenAI-compatible API endpoints.
+
+ Args:
+ model_key: The key of the model to serve from the MODELS dictionary.
+ """
+ import subprocess
+
+ model_config = MODELS.get(model_key)
+ if not model_config:
+ raise ValueError(f"Model {model_key} not found in MODELS dictionary")
+
+ model_name = model_config["name"]
+ model_revision = model_config["revision"]
+
+ cmd = [
+ "vllm",
+ "serve",
+ "--uvicorn-log-level=info",
+ model_name,
+ ]
+
+ if model_revision:
+ cmd.extend(["--revision", model_revision])
+
+ cmd.extend([
+ "--host", "0.0.0.0",
+ "--port", str(VLLM_PORT),
+ "--api-key", API_KEY,
+ ])
+
+ # Optional parameters - uncomment and adjust as needed
+ # cmd.extend(["--max-model-len", "15000"])
+ # cmd.extend(["--guided-decoding-backend", "outlines"])
+ # cmd.extend(["--tensor-parallel-size", "1"]) # For multi-GPU
+
+ subprocess.Popen(" ".join(cmd), shell=True)
+
+@app.function(
+ image=vllm_image,
+ gpu=MODELS["phi4"]["gpu"], # Use the GPU configuration from the specified model
+ volumes={
+ "/root/.cache/huggingface": hf_cache_vol,
+ "/root/.cache/vllm": vllm_cache_vol,
+ },
+ scaledown_window=15 * MINUTES, # How long to wait with no traffic before scaling down
+)
+@modal.concurrent(max_inputs=100) # How many requests can one replica handle
+@modal.web_server(port=VLLM_PORT, startup_timeout=5 * MINUTES)
+def serve_phi4():
+ """Serves the Phi-4 model with OpenAI-compatible API endpoints."""
+ import subprocess
+
+ model_config = MODELS["phi4"]
+ model_name = model_config["name"]
+
+ cmd = [
+ "vllm",
+ "serve",
+ "--uvicorn-log-level=info",
+ model_name,
+ "--host", "0.0.0.0",
+ "--port", str(VLLM_PORT),
+ "--api-key", API_KEY,
+ "--max-model-len", "15000",
+ "--guided-decoding-backend", "outlines",
+ ]
+
+ subprocess.Popen(" ".join(cmd), shell=True)
+
+@app.function(
+ image=vllm_image,
+ gpu="A10G:1", # Embeddings also need GPU access
+ volumes={
+ "/root/.cache/huggingface": hf_cache_vol,
+ "/root/.cache/vllm": vllm_cache_vol,
+ },
+ scaledown_window=15 * MINUTES, # How long to wait with no traffic before scaling down
+)
+@modal.concurrent(max_inputs=100) # How many requests can one replica handle
+@modal.web_server(port=VLLM_PORT, startup_timeout=5 * MINUTES)
+def embeddings():
+ """Serves the embeddings model with OpenAI-compatible API endpoints."""
+ import subprocess
+
+ model_name = "mixedbread-ai/mxbai-embed-xsmall-v1"
+
+ cmd = [
+ "vllm",
+ "serve",
+ "--uvicorn-log-level=info",
+ model_name,
+ "--host", "0.0.0.0",
+ "--port", str(VLLM_PORT),
+ "--api-key", API_KEY,
+ "--guided-decoding-backend", "outlines",
+ "--trust-remote-code",
+ ]
+
+ subprocess.Popen(" ".join(cmd), shell=True)
+
+@app.local_entrypoint()
+def main(model_key="phi4", test=True):
+ """
+ Local entrypoint for testing the Modal server.
+
+ Args:
+ model_key: The key of the model to serve from the MODELS dictionary.
+ test: Whether to run a test request against the server.
+ """
+ import json
+ import time
+ import urllib
+
+ # Use the specific model endpoint if available, otherwise use the generic one
+ if model_key == "phi4":
+ serve_function = serve_phi4
+ else:
+ # This line would be used for a generic function that can serve any model
+ # In this simplified example, we don't define it that way
+ raise ValueError(f"No specific endpoint for model {model_key}. Use phi4 or extend the script.")
+
+ print(f"Starting server for model {model_key} at {serve_function.web_url}")
+
+ if test:
+ # Test the server with a health check
+ print(f"Running health check for server at {serve_function.web_url}")
+ up, start, delay = False, time.time(), 10
+ test_timeout = 5 * MINUTES
+
+ while not up:
+ try:
+ with urllib.request.urlopen(serve_function.web_url + "/health") as response:
+ if response.getcode() == 200:
+ up = True
+ except Exception:
+ if time.time() - start > test_timeout:
+ break
+ time.sleep(delay)
+
+ assert up, f"Failed health check for server at {serve_function.web_url}"
+ print(f"Successful health check for server at {serve_function.web_url}")
+
+ # Test with a sample message
+ messages = [{"role": "user", "content": "Testing! Is this thing on?"}]
+ print(f"Sending a sample message to {serve_function.web_url}", *messages, sep="\n")
+
+ headers = {
+ "Authorization": f"Bearer {API_KEY}",
+ "Content-Type": "application/json",
+ }
+ payload = json.dumps({"messages": messages, "model": MODELS[model_key]["name"]})
+ req = urllib.request.Request(
+ serve_function.web_url + "/v1/chat/completions",
+ data=payload.encode("utf-8"),
+ headers=headers,
+ method="POST",
+ )
+ with urllib.request.urlopen(req) as response:
+ print(json.loads(response.read().decode()))
+
+# To deploy:
+# modal deploy modal_inference.py
+
+# To test:
+# modal run modal_inference.py
+
+# Client usage example:
+# from openai import OpenAI
+# client = OpenAI(
+# api_key="comind-api-key", # Must match API_KEY above
+# base_url="https://yourworkspace--comind-vllm-inference-serve-phi4.modal.run/v1"
+# )
+# response = client.chat.completions.create(
+# model="microsoft/Phi-4",
+# messages=[{"role": "user", "content": "Hello, how are you?"}]
+# )
+# print(response.choices[0].message.content)
\ No newline at end of file
diff --git a/prompts/cominds/conceptualizer.co b/prompts/cominds/conceptualizer.co
index 4e67cb9..c5aa666 100644
--- a/prompts/cominds/conceptualizer.co
+++ b/prompts/cominds/conceptualizer.co
@@ -9,54 +9,50 @@
{links}
-
## Your role
-You are a conceptualizer, meaning your expansion should include a list
-of new concepts related to the current node.
-
-Concepts are extremely short words or phrases that are related to the
-current node. Concepts must be lowercase and may contain spaces. You
-should think of concepts as abstractions or labels for the current node.
-
-Your role as a conceptualizer is to interconnect thoughts across cominds
-and to create a more comprehensive understanding of the current node.
+You are a conceptualizer within the {sphere_name} sphere, guided by the core perspective: "{core_perspective}"
-Concepts form the core of the comind network -- without them, the
-network will spread out and lose its focus.
+Your task is to extract concepts that:
+1. Relate to the current content
+2. Meaningfully connect to your sphere's core perspective
+3. Create useful bridges between ideas in the network
-## Guidelines for selecting concepts
+Concepts are extremely short words or phrases (typically 1-3 words) that must be lowercase and may contain spaces. They serve as semantic anchors that allow different contents to be connected through shared abstractions.
-- Concepts should feel related to the core directive of "be"
-- Generate 3-15 concepts based on content complexity
-- Include both specific concepts (directly mentioned) and abstract concepts (implied themes)
-- Prioritize concepts that enable connections to other domains of knowledge
-- Balance breadth and specificity - include different categories of concepts
-- Use simple, clear language for concepts
-- Prefer shorter concepts (1-3 words) when possible
-- Avoid duplicative concepts (choose the most accurate one)
+Your core directive shapes how you perceive and prioritize concepts. Each concept you identify should illuminate some aspect of the content through the lens of your directive.
-## Your response format
+## Guidelines for concept selection
-Your response should be a JSON object with an array of concepts
-and their connections to the content.
+- Generate 5-15 concepts based on content complexity
+- Each concept MUST have a clear connection to your sphere's core directive
+- Prioritize concepts that could connect this content to other nodes in your directive's domain
+- Include both:
+ * Direct concepts (explicitly mentioned in the content)
+ * Implicit concepts (themes, principles, or abstractions suggested by the content)
+- For each concept, explain HOW it connects to your core directive
+- Assign a relevance strength (0-1) indicating how strongly each concept aligns with your directive
+- Concepts should form a semantic network around the content that reflects your sphere's perspective
-## Examples of good concepts
+## Example concept format
-For content about a new solar-powered drone:
-- "renewable energy" (broader category)
-- "aviation" (domain)
-- "solar technology" (specific technology)
-- "surveillance" (potential application)
-- "autonomy" (characteristic)
+```json
+"text": "distributed cognition",
+"connection_to_content": (
+ "relationship": "PART_OF",
+ "strength": 0.85,
+ "note": "The content discusses systems for processing information across multiple agents"
+)
+"alignment_with_directive": (
+ "strength": 0.9,
+ "explanation": "This concept directly relates to our directive of understanding collective intelligence by highlighting how cognition can be distributed across a network"
+)
+```
-For content about protein folding in biology:
-- "biochemistry" (domain)
-- "molecular structure" (broader concept)
-- "proteins" (central topic)
-- "3d modeling" (related technique)
-- "computational biology" (interdisciplinary connection)
+## Remember
+Concepts form the backbone of the comind network - they allow connections to form across disparate content. Without well-chosen concepts that align with your directive, your sphere would lose focus and coherence.
+The concepts you identify will shape how this content connects to the broader knowledge graph within your perspective.
@@ -68,6 +64,13 @@ For content about protein folding in biology:
## Instructions
-Generate a list of concepts related to the task-specific context.
+Analyze this content through the lens of your core perspective: "{core_perspective}"
+Extract a diverse set of concepts that:
+
+- Represent key ideas within the content
+- Connect to our sphere's perspective and purpose
+- Would enable useful connections to other content in our knowledge domain
+
+For each concept, explain both its connection to the content and how it aligns with our perspective.
diff --git a/prompts/cominds/thinker.co b/prompts/cominds/thinker.co
index bfb0e75..25ded86 100644
--- a/prompts/cominds/thinker.co
+++ b/prompts/cominds/thinker.co
@@ -1,39 +1,92 @@
+
+
{comind_network}
-{core}
+## Your role and perspective
+
+You are a thinker comind operating within the {sphere_name} sphere, guided by the core perspective: "{core_perspective}"
+
+This perspective shapes how you perceive, process, and respond to all information. Every thought you generate should be influenced by this perspective, creating a consistent cognitive lens that distinguishes your thinking from other spheres.
+
+As a thinker, you generate structured thoughts that represent deep analysis rather than surface observations. Your thoughts are substantive, evidence-based cognitive artifacts that become part of a permanent knowledge graph, connecting ideas across the network through your sphere's unique perspective.
+
+## Thought types
+
+Your thoughts must be classified according to one of these cognitive processes:
+
+- **analysis**: Breaking down complex information into constituent parts to understand structure and relationships
+- **prediction**: Forecasting future states or developments based on current patterns
+- **evaluation**: Assessing value, quality, or significance against specific criteria
+- **comparison**: Examining similarities and differences between entities or concepts
+- **inference**: Drawing conclusions based on evidence and reasoning
+- **critique**: Offering constructive examination of limitations, flaws, or weaknesses
+- **integration**: Combining disparate elements into a coherent whole
+- **speculation**: Theoretical consideration of possibilities without definitive evidence
+- **clarification**: Making complex or ambiguous information more understandable
+- **metacognition**: Reflecting on the process of thinking itself
+- **observation**: Direct perception and description of phenomena
+- **reflection**: Contemplative consideration of implications or meanings
+- **hypothesis**: Proposing explanatory frameworks that can be tested
+- **question**: Formulating inquiries that guide further exploration
+- **synthesis**: Creating new understanding by combining existing knowledge
+- **correction**: Identifying and addressing errors or misconceptions
-{links}
+## Thought structure and quality
-## Your role
+Each thought you generate must include:
-You are a thinker comind in the Comind network. Your purpose is to generate insightful thoughts about content flowing through the network.
+1. **ThoughtType**: One of the cognitive processes listed above
+2. **Text**: A clear, substantive statement that captures your perspective
+3. **Context**: Background information needed to understand the thought
+4. **Evidence**: Facts, observations, or reasoning that justify your perspective (list format)
+5. **Alternatives**: Recognition of other valid interpretations (list format)
+6. **perspective alignment**: Explicit explanation of how this thought connects to your core perspective
-Thoughts are structured cognitive artifacts that capture reasoned, nuanced perspectives on information. Unlike simple reactions or classifications, thoughts represent deeper analysis and understanding with supporting evidence and acknowledgment of alternative viewpoints.
+High-quality thoughts should:
+- Provide non-obvious insights rather than restating the obvious
+- Make specific claims rather than vague generalizations
+- Connect to broader patterns within your sphere's domain
+- Identify implications that extend beyond immediate content
+- Recognize tensions, contradictions, or nuances
+- Maintain coherence with your sphere's established perspective
-Your thoughts should:
-1. Clearly identify the type of cognitive process represented (analysis, prediction, evaluation, etc.)
-2. Provide substantive, specific content rather than generic observations
-3. Include relevant context for understanding the thought
-4. Reference evidence that supports your perspective
-5. Acknowledge alternative interpretations when appropriate
+## perspective-guided thinking
-When forming thoughts, consider:
-- Connections to broader themes or patterns in the network
-- Implications that might not be immediately obvious
-- Potential contradictions or tensions within the content
-- Historical or social context relevant to understanding
+Your core perspective of "{core_perspective}" should influence your thinking in several ways:
-Remember that your thoughts will be linked to other cognitive artifacts throughout the network, so clarity and precision are essential. Your thoughts should enrich the knowledge graph by providing substantive connections between ideas.
+1. **Priority filtering** - Which aspects of the content you focus on
+2. **Conceptual framing** - How you interpret and categorize information
+3. **Value alignment** - The evaluative criteria you apply
+4. **Connection patterns** - How you relate this content to other knowledge
+5. **Language selection** - The terminology and tone you employ
+
+For each thought, explicitly articulate how it aligns with your perspective and assign a strength value (0-1) indicating how strongly it embodies your sphere's perspective.
+
+## Remember
+
+Your thoughts shape the evolving character of your sphere. Consistently viewing information through your perspective's lens creates a coherent cognitive environment with a distinctive perspective. This coherence is what makes your sphere valuable as a specialized cognitive workspace within the broader network.
-As a thinker comind, you represent the analytical layer of the cognitive network - your role is to process information deeply rather than merely react to it.
-
-Please generate thoughtful, substantive thoughts about this content:
+
+## Current content
{content}
-
+## Instructions
+Generate 3-5 substantive thoughts about this content through the lens of our core perspective: "{core_perspective}"
+
+For each thought:
+1. Classify its cognitive type (from the enumerated list)
+2. Provide a meaningful insight (not just summary)
+3. Include relevant context
+4. Reference supporting evidence
+5. Acknowledge alternative perspectives
+6. Explain how the thought aligns with our perspective
+
+Ensure your thoughts represent diverse cognitive processes and explore different aspects of the content. Each thought should contribute to our sphere's evolving understanding of this domain.
+
+
\ No newline at end of file
diff --git a/src/comind/comind.py b/src/comind/comind.py
index 94cddac..07ef9f9 100644
--- a/src/comind/comind.py
+++ b/src/comind/comind.py
@@ -15,6 +15,7 @@ from typing import Optional
from rich import print
from rich.panel import Panel
from src.comind.logging_config import configure_logger_without_timestamp, configure_root_logger_without_timestamp
+from .format import format, format_dict
# Configure root logger without timestamps - this affects all logging in the application
configure_root_logger_without_timestamp()
@@ -28,8 +29,8 @@ class Comind:
common_prompt_dir: str
logger: logging.Logger
core_perspective: str = None
-
- def __init__(self, name: str, prompt_path: str = None, common_prompt_dir: str = None, core_perspective: str = None):
+ sphere_name: str = None
+ def __init__(self, name: str, prompt_path: str = None, common_prompt_dir: str = None, core_perspective: str = None, sphere_name: str = None):
self.name = name
if prompt_path is None:
@@ -44,20 +45,23 @@ class Comind:
if core_perspective is not None:
self.core_perspective = core_perspective
+
+ if sphere_name is not None:
+ self.sphere_name = sphere_name
# Initialize logger with basename of the .co file
basename = os.path.basename(self.prompt_path).replace(".co", "")
self.logger = configure_logger_without_timestamp(basename)
@classmethod
- def load(cls, name: str):
+ def load(cls, name: str, sphere_name: str = None):
"""Takes a name and attempts to return the specialized comind class"""
if name == "conceptualizer":
- return Conceptualizer()
+ return Conceptualizer(sphere_name=sphere_name)
elif name == "feeler":
- return Feeler()
+ return Feeler(sphere_name=sphere_name)
elif name == "thinker":
- return Thinker()
+ return Thinker(sphere_name=sphere_name)
else:
raise ValueError(f"Unknown comind: {name}")
@@ -75,8 +79,17 @@ class Comind:
if self.core_perspective is not None and "core_perspective" not in common_prompts:
common_prompts["core_perspective"] = self.core_perspective
+ if self.sphere_name is not None and "sphere_name" not in common_prompts:
+ common_prompts["sphere_name"] = self.sphere_name
+
return common_prompts
+ def get_required_context_keys(self):
+ """
+ Returns a list of keys that are required for the prompt.
+ """
+ return ["core_perspective", "sphere_name"]
+
def to_prompt(self, context_dict: dict):
"""
Load and format the prompt with values from context_dict.
@@ -110,22 +123,22 @@ class Comind:
raise ValueError("Core perspective is required but was None")
# Format the common prompts
- for common_key, common_content in common_prompts.items():
- context_dict[common_key] = common_content.format(**context_dict)
+ context_dict.update(format_dict(common_prompts, context_dict))
# Log the keys available in context_dict to help with debugging
- self.logger.debug(f"Context keys: {list(context_dict.keys())}")
-
+ self.logger.info(f"Context keys: {list(context_dict.keys())}")
+
try:
# Actually format the prompt with the values from context_dict
formatted_prompt = raw_prompt.format(**context_dict)
return formatted_prompt
except KeyError as e:
- self.logger.error(f"Missing key in context_dict for prompt formatting: {e}")
+ self.logger.error(f"Expected key in context_dict for prompt formatting: {e}")
# Continue without raising, return the raw prompt
- self.logger.warning("Returning unformatted prompt due to missing key")
- return raw_prompt
-
+ # self.logger.warning("Returning unformatted prompt due to missing key")
+ # return raw_prompt
+ raise e
+
def split_prompts(self, context_dict: dict = {}, format: bool = True):
"""
Splits a co file into system, schema, and user messages.
@@ -181,8 +194,17 @@ class Comind:
Returns:
The generated result
"""
+
+ # Add the core_perspective to the context_dict
+ context_dict["core_perspective"] = self.core_perspective
+
+ # Add the sphere_name to the context_dict
+ context_dict["sphere_name"] = self.sphere_name
+
prompts = self.split_prompts(context_dict)
messages = self.messages(prompts)
+
+ self.logger.debug("messages", messages)
# Debug log to see the actual prompts being sent
if prompts["system"]:
@@ -206,6 +228,13 @@ class Comind:
schema = self.schema()
+ # # Format the text to interpolation the context_dict
+ # # replaces {key} with value.
+ # for message in messages:
+ # print("message", message)
+ # message["content"] = message["content"].format(**context_dict)
+ # print(messages)
+
return sg.generate_by_schema(messages, schema)
def available_cominds():
@@ -215,11 +244,13 @@ def available_cominds():
return cominds
class Conceptualizer(Comind):
- def __init__(self):
+ def __init__(self, sphere_name: str = None, core_perspective: str = None, prompt_path: str = None, common_prompt_dir: str = None):
super().__init__(
name="conceptualizer",
prompt_path="prompts/cominds/conceptualizer.co",
common_prompt_dir="prompts/common/",
+ sphere_name=sphere_name,
+ core_perspective=core_perspective,
)
def schema(self):
@@ -329,11 +360,13 @@ Connection to content: {connection_to_content}
self.logger.debug(f"Link creation result: {record_result}")
class Feeler(Comind):
- def __init__(self):
+ def __init__(self, sphere_name: str = None, core_perspective: str = None):
super().__init__(
name="feeler",
prompt_path="prompts/cominds/feeler.co",
common_prompt_dir="prompts/common/",
+ sphere_name=sphere_name,
+ core_perspective=core_perspective,
)
def schema(self):
@@ -433,10 +466,12 @@ Connection to content: {connection_to_content}
self.logger.debug(f"Link creation result: {record_result}")
class Thinker(Comind):
- def __init__(self):
+ def __init__(self, sphere_name: str = None, core_perspective: str = None):
super().__init__(
name="thinker",
prompt_path="prompts/cominds/thinker.co",
+ sphere_name=sphere_name,
+ core_perspective=core_perspective,
)
def schema(self):
@@ -481,17 +516,17 @@ class Thinker(Comind):
# Upload the concept to the Comind network
log_base_str = f"{thought_type}"
if thought_text:
- log_base_str += f" - {thought_text}"
+ log_base_str += f" - text: {thought_text}"
if thought_relationship:
- log_base_str += f" - {thought_relationship}"
+ log_base_str += f" - relationship: {thought_relationship}"
if thought_note:
- log_base_str += f" - {thought_note}"
+ log_base_str += f" - note: {thought_note}"
if context:
- log_base_str += f" - {context}"
+ log_base_str += f" - context: {context}"
if evidence:
- log_base_str += f" - {evidence}"
+ log_base_str += f" - evidence: {evidence}"
if alternatives:
- log_base_str += f" - {alternatives}"
+ log_base_str += f" - alternatives: {alternatives}"
self.logger.info(log_base_str)
# Create printout string
@@ -569,10 +604,12 @@ if __name__ == "__main__":
for comind in cominds:
try:
core_perspective = record_manager.get_perspective()
+ core_name = record_manager.get_sphere_name()
if not core_perspective:
raise ValueError("Core perspective was retrieved but is empty")
# Print the first 100 characters of the core perspective for debugging
+ print(f"Core name: {core_name}")
print(f"Core perspective (first 100 chars): {core_perspective[:100]}...")
except Exception as e:
comind.logger.error(f"Failed to get core perspective: {e}")
@@ -605,11 +642,11 @@ if __name__ == "__main__":
context_dict = {
"content": prompt,
"core_perspective": core_perspective,
+ "sphere_name": core_name,
}
try:
result = comind.run(context_dict)
- print(result)
if isinstance(comind, Feeler):
for emotion in result["emotions"]:
@@ -672,4 +709,7 @@ if __name__ == "__main__":
print(f"[red]Error processing post:[/red] {e}")
print(f"Post: {post}")
# Continue with next post rather than crashing
- continue
+ # continue
+
+ # Raise the error
+ raise e
diff --git a/src/comind/format.py b/src/comind/format.py
new file mode 100644
index 0000000..dfc31b8
--- /dev/null
+++ b/src/comind/format.py
@@ -0,0 +1,107 @@
+"""
+Format utilities for the Comind project.
+Provides a more robust alternative to Python's string.format method.
+"""
+
+import re
+import logging
+from typing import Any, Dict, Optional, Union
+
+logger = logging.getLogger(__name__)
+
+def format(template: str, context: Dict[str, Any],
+ safe: bool = True, default: str = "",
+ recursive: bool = True, max_depth: int = 5) -> str:
+ """
+ Format a template string using values from a context dictionary.
+
+ This is a more robust alternative to Python's string.format() method,
+ with features like:
+ - Safe formatting (won't raise KeyError)
+ - Default values for missing keys
+ - Recursive formatting (format placeholders within formatted values)
+ - Maximum recursion depth to prevent infinite loops
+ - Detailed logging of formatting errors
+
+ Args:
+ template: The template string with {placeholders}
+ context: Dictionary with values to insert into the template
+ safe: If True, missing keys won't raise an exception
+ default: Default value to use for missing keys when safe=True
+ recursive: If True, perform recursive formatting
+ max_depth: Maximum recursion depth for recursive formatting
+
+ Returns:
+ The formatted string
+ """
+ if not template:
+ return template
+
+ if not isinstance(template, str):
+ logger.warning(f"Non-string template passed to format: {type(template)}")
+ return str(template)
+
+ # Find all placeholders in the string
+ placeholders = re.findall(r'\{([^{}]*)\}', template)
+
+ # Process each placeholder
+ result = template
+ for placeholder in placeholders:
+ try:
+ # Handle format specifiers (e.g., {key:10s})
+ key = placeholder.split(':', 1)[0]
+
+ if key in context:
+ value = context[key]
+
+ # Convert value to string if it's not already
+ value_str = str(value) if not isinstance(value, str) else value
+
+ # Handle recursive formatting if needed
+ if recursive and max_depth > 0 and '{' in value_str and '}' in value_str:
+ value_str = format(
+ value_str,
+ context,
+ safe=safe,
+ default=default,
+ recursive=recursive,
+ max_depth=max_depth-1
+ )
+
+ # Replace the placeholder with the value
+ result = result.replace(f"{{{placeholder}}}", value_str)
+ elif safe:
+ # Replace with default value if the key is missing
+ result = result.replace(f"{{{placeholder}}}", default)
+ else:
+ # Raise an error for missing keys if not in safe mode
+ raise KeyError(f"Missing key '{key}' in context dictionary")
+
+ except Exception as e:
+ if safe:
+ logger.warning(f"Error formatting placeholder '{placeholder}': {str(e)}")
+ result = result.replace(f"{{{placeholder}}}", default)
+ else:
+ raise
+
+ return result
+
+def format_dict(templates: Dict[str, str], context: Dict[str, Any], **kwargs) -> Dict[str, str]:
+ """
+ Format all string values in a dictionary using the same context.
+
+ Args:
+ templates: Dictionary of template strings to format
+ context: Context dictionary to use for formatting
+ **kwargs: Additional arguments to pass to format()
+
+ Returns:
+ Dictionary with all string values formatted
+ """
+ result = {}
+ for key, template in templates.items():
+ if isinstance(template, str):
+ result[key] = format(template, context, **kwargs)
+ else:
+ result[key] = template
+ return result
\ No newline at end of file
diff --git a/src/jetstream_consumer.py b/src/jetstream_consumer.py
index 802b204..0e45145 100644
--- a/src/jetstream_consumer.py
+++ b/src/jetstream_consumer.py
@@ -267,7 +267,7 @@ async def process_event(
root_post_uri: str = None,
thread_depth: int = 15,
user_info_cache: UserInfoCache = None,
- comind: Comind = None
+ comind: Comind = None,
) -> None:
"""Process an event and generate thoughts, emotions, and concepts for it"""
try:
@@ -646,7 +646,6 @@ async def main():
spheres = record_manager.list_records("me.comind.sphere.core")
sphere_to_use = None
-
logger.debug(f"Found {len(spheres)} spheres")
for sphere in spheres:
value = sphere.value
@@ -679,7 +678,13 @@ async def main():
# Create the comind
comind = Comind.load(args.comind)
- comind.core_perspective = sphere_to_use.value["text"]
+
+ if sphere_to_use is not None:
+ comind.sphere_name = sphere_to_use.value["title"]
+ comind.sphere_description = sphere_to_use.value["description"]
+ comind.core_perspective = sphere_to_use.value["text"]
+ else:
+ logger.warning("No sphere provided. Comind will not be attached to any sphere.")
try:
await connect_to_jetstream(
@@ -693,7 +698,7 @@ async def main():
logger.info("Shutting down")
except Exception as e:
logger.error(f"Unexpected error: {e}")
- raise e
+ # raise e
if __name__ == "__main__":
diff --git a/src/record_manager.py b/src/record_manager.py
index 2b4005e..571443d 100644
--- a/src/record_manager.py
+++ b/src/record_manager.py
@@ -99,6 +99,15 @@ class RecordManager:
else:
return None
+ def get_sphere_name(self):
+ """
+ Get the sphere name from the sphere record.
+ """
+ record = self.get_sphere_record()
+ if record:
+ return record.value['title']
+ else:
+ return None
def try_get_record(self, collection: str, rkey: str) -> Optional[Dict]:
"""
@@ -192,7 +201,7 @@ class RecordManager:
self.sphere_record(response.uri, self.sphere_uri)
)
- logger.debug(f"Successfully created {collection} record https://atp.tools/{response.uri}")
+ logger.info(f"Successfully created {collection} record https://atp.tools/{response.uri}")
logger.debug(f"Rate limiting: sleeping for {RATE_LIMIT_SLEEP_SECONDS} seconds")
time.sleep(RATE_LIMIT_SLEEP_SECONDS)
return response