Skip to main content
Version: Next

Financial Advisor Agent with MeTTa

Overview

This guide shows how to integrate SingularityNET's MeTTa (Meta Type Talk) knowledge graph with Fetch.ai's uAgents framework. The sample is a toy demo: it looks up illustrative risk → investment → return / allocation / goal facts in MeTTa, then uses ASI:One to classify intent and humanize the reply. It is not financial advice, not personalized recommendations, and is not a substitute for a licensed advisor.

Shared MeTTa + uAgent template: same layout and safety pattern as Medical Agent with MeTTa and Fetch.ai Knowledge Assistant with MeTTa (sibling review: issue #260).

Tested combo: Python 3.10–3.12, uagents>=0.25.5 (needs uagents-core 0.4.x), hyperon>=0.2.6. Chat Protocol samples need this runtime; Python 3.8 is not supported.

What is MeTTa?

MeTTa (Meta Type Talk) is SingularityNET's multi-paradigm language for declarative and functional computations over knowledge (meta)graphs. Official docs: MeTTa language and Hyperon. It provides:

  • Structured Knowledge Representation: Organize information in logical, queryable formats
  • Symbolic Reasoning: Perform complex logical operations and pattern matching
  • Knowledge Graph Operations: Build, query, and manipulate knowledge graphs
  • Space-based Architecture: Knowledge stored as atoms in logical spaces

Installation & Setup

Prerequisites

Before you begin, ensure you have:

  • Python 3.10+ (3.10–3.12 recommended for uagents 0.25.x). On Windows use py -3.10; on WSL/macOS/Linux use python3.
  • pip package manager
  • An ASI:One API key from the ASI:One API keys dashboard (not only the asi1.ai homepage)

Create a project folder and a virtual environment (do not install into system Python):

# macOS / Linux / WSL
python3 -m venv .venv
source .venv/bin/activate

# Windows PowerShell
py -3.10 -m venv .venv
.\.venv\Scripts\Activate.ps1

Create a .env file (never commit real secrets):

ASI_ONE_API_KEY=your_key_here
AGENT_SEED=change-me-to-a-unique-local-seed
# Optional: set LEARN=1 only if you want the demo to persist LLM guesses into the graph
# LEARN=1

Installation Options

Create a requirements.txt file with one package per line:

openai>=1.0.0
hyperon>=0.2.6
uagents>=0.25.5
uagents-core>=0.4.9
python-dotenv>=1.0.0

uagents 0.25.x is tested with uagents-core 0.4.x (Chat Protocol). Keep uagents>=0.25.5 and uagents-core>=0.4.9 unless you intentionally upgrade the whole stack.

Install all dependencies with one command:

python3 -m pip install -r requirements.txt

On Windows: py -3.10 -m pip install -r requirements.txt.


Option 2: Verify Hyperon First

Use this only to confirm Hyperon/MeTTa installs on your machine. You still need Option 1 (requirements.txt) for uagents, openai, and python-dotenv.

python3 -m pip install hyperon
python3 -c "from hyperon import MeTTa; print('Hyperon installed successfully!')"

Windows Installation Guide

Hyperon on native Windows is often painful. WSL (Ubuntu) is recommended. If you stay on native Windows and hit build errors, see this video: Hyperon Installation on Windows.

Written WSL path:

  1. Install WSL and Ubuntu.
  2. Inside WSL: install Python 3.10+, create the venv above, then pip install -r requirements.txt.
  3. Run python3 agent.py from the project folder shown below.

Project layout

Imports in agent.py use the metta package. Create this tree (a flat folder of four .py files will raise ImportError):

project/
agent.py
metta/
__init__.py
knowledge.py
investment_rag.py
utils.py
.env
requirements.txt

Create empty metta/__init__.py. Run from project/:

python3 agent.py

Windows: py -3.10 agent.py.

This page is the canonical sample. Copy the files below into that tree.

Architecture Overview

Financial Advisor Agent — sequence-style workflow (yellow / green / white)

The code pipeline (ASI:One chat does not classify intent for you):

flowchart LR
user[User or ASI:One chat]
handler[Chat Protocol handler]
llm[Agent LLM: intent plus keyword]
lookup[MeTTa knowledge lookup]
humanize[Humanize plus disclaimer]
user --> handler --> llm --> lookup --> humanize --> user

Alt text: User or ASI:One sends chat text to the Chat Protocol handler. The agent LLM classifies intent and a keyword, MeTTa looks up the toy graph, then the agent humanizes the answer and sends a disclaimer-prefixed reply.

Architecture pipeline: User / ASI:One Chat → Chat Protocol handler → agent LLM classifies intent + keyword → MeTTa knowledge lookup (not vector RAG) → humanized reply with disclaimer → User.

Core Integration Concepts

1. MeTTa Knowledge Graph Structure

MeTTa organizes knowledge as atoms in logical spaces. Use one convention: risk profiles, investment types, age buckets, goals, and FAQ keys as S(...); free-text returns, risk notes, allocations, and FAQ answers as ValueAtom. Multi-word names use underscores (emergency_fund, dividend_stocks), never raw spaces or parentheses inside query interpolation.

from hyperon import MeTTa, E, S, ValueAtom

metta = MeTTa()

metta.space().add_atom(E(S("risk_profile"), S("conservative"), S("bonds")))
metta.space().add_atom(
E(S("expected_return"), S("bonds"), ValueAtom("illustrative 3-5% annually (toy range)"))
)
metta.space().add_atom(
E(S("faq"), S("amount_to_invest"), ValueAtom("Toy tip: 10-20% of income after an emergency fund."))
)

Key MeTTa Elements:

  • E (Expression): Creates logical expressions
  • S (Symbol): Represents symbolic atoms (profiles, investment types, FAQ keys)
  • ValueAtom: Stores string values (return ranges, allocation text, FAQ answers)
  • Space: Container where atoms are stored and queried

2. Pattern Matching and Querying

# Query syntax: !(match &self (relation subject $variable) $variable)
query_str = '!(match &self (risk_profile conservative $investment) $investment)'
results = metta.run(query_str)
# Results include bonds, dividend_stocks, savings_accounts for the toy graph

Query Components:

  • &self: References the current space
  • $variable: Pattern matching variables that capture results
  • !(match ...): Query syntax for pattern matching

Never interpolate unsanitized user/LLM text into MeTTa. Only simple [a-z0-9_]+ symbols are allowed.

3. uAgent Chat Protocol Integration

The following is an excerpt. Full Protocol construction is in agent.py. process_query returns a dict; send the humanized_answer string (plus disclaimer), not the dict.

from uagents_core.contrib.protocols.chat import (
ChatMessage,
ChatAcknowledgement,
TextContent,
chat_protocol_spec,
)

@chat_proto.on_message(ChatMessage)
async def handle_message(ctx: Context, sender: str, msg: ChatMessage):
response = process_query(user_query, rag, llm)
answer = response.get("humanized_answer", "I could not process that query.")
await ctx.send(sender, create_text_chat(answer))

mailbox=True on Agent(...) is the Agentverse mailbox flag (an inbox so a local agent stays reachable). Do not import mailbox — that is Python's stdlib email-mailbox module and is unused here.

publish_agent_details=True publishes the agent's profile/details to Agentverse when the mailbox connects. Use it for discoverable demos; turn it off if you do not want the profile updated automatically.

4. Knowledge lookup (not vector RAG)

InvestmentRAG in this sample is a MeTTa retriever: pattern-match on the toy graph, then an LLM humanizes the result. It does not use embeddings or document RAG. The class name is historical; treat it as knowledge lookup. Dynamic graph writes from LLM guesses are off unless LEARN=1 is set (unsafe for anything beyond a local experiment).

Core Components

  1. agent.py: Main uAgent with Chat Protocol
  2. metta/knowledge.py: Toy MeTTa graph (illustrative US-centric teaching data — not market facts)
  3. metta/investment_rag.py: MeTTa lookup helpers
  4. metta/utils.py: Intent classification and query processing

Implementation Guide

Step 1: Define Your Knowledge Domain

Create metta/knowledge.py. Seed edges use symbols for investment types under risk_profile, and ValueAtom for free-text returns, risk notes, allocations, strategies, sector examples, mistakes, and FAQs. FAQ keys must match query_faq (not the raw user sentence).

from hyperon import MeTTa, E, S, ValueAtom


def initialize_investment_knowledge(metta: MeTTa):
"""Toy graph for the tutorial. Not financial advice or market data."""
# Risk profile → investment types (symbols)
metta.space().add_atom(E(S("risk_profile"), S("conservative"), S("bonds")))
metta.space().add_atom(E(S("risk_profile"), S("conservative"), S("dividend_stocks")))
metta.space().add_atom(E(S("risk_profile"), S("conservative"), S("savings_accounts")))
metta.space().add_atom(E(S("risk_profile"), S("moderate"), S("index_funds")))
metta.space().add_atom(E(S("risk_profile"), S("moderate"), S("etfs")))
metta.space().add_atom(E(S("risk_profile"), S("moderate"), S("real_estate")))
metta.space().add_atom(E(S("risk_profile"), S("aggressive"), S("growth_stocks")))
metta.space().add_atom(E(S("risk_profile"), S("aggressive"), S("cryptocurrency")))
metta.space().add_atom(E(S("risk_profile"), S("aggressive"), S("options")))

# Investment → illustrative return ranges (not forecasts)
metta.space().add_atom(
E(S("expected_return"), S("bonds"), ValueAtom("illustrative 3-5% annually (toy range)"))
)
metta.space().add_atom(
E(
S("expected_return"),
S("dividend_stocks"),
ValueAtom("illustrative 5-7% annually (toy range)"),
)
)
metta.space().add_atom(
E(
S("expected_return"),
S("index_funds"),
ValueAtom("illustrative 6-10% annually (toy range)"),
)
)
metta.space().add_atom(
E(S("expected_return"), S("etfs"), ValueAtom("illustrative 5-12% annually (toy range)"))
)
metta.space().add_atom(
E(
S("expected_return"),
S("growth_stocks"),
ValueAtom("illustrative 8-15% annually (toy range)"),
)
)
metta.space().add_atom(
E(
S("expected_return"),
S("cryptocurrency"),
ValueAtom("illustrative: highly volatile (toy label)"),
)
)
metta.space().add_atom(
E(
S("expected_return"),
S("savings_accounts"),
ValueAtom("illustrative 1-2% annually (toy range)"),
)
)
metta.space().add_atom(
E(
S("expected_return"),
S("real_estate"),
ValueAtom("illustrative 4-8% annually (toy range)"),
)
)
metta.space().add_atom(
E(
S("expected_return"),
S("options"),
ValueAtom("illustrative: high risk, leveraged (toy label)"),
)
)

# Investment → risk notes (softened; US-centric toy wording)
metta.space().add_atom(
E(S("risk_level"), S("bonds"), ValueAtom("typically lower volatility in this toy graph"))
)
metta.space().add_atom(
E(
S("risk_level"),
S("dividend_stocks"),
ValueAtom("low-moderate risk label in this toy graph"),
)
)
metta.space().add_atom(
E(S("risk_level"), S("index_funds"), ValueAtom("moderate risk, diversified (toy label)"))
)
metta.space().add_atom(
E(S("risk_level"), S("etfs"), ValueAtom("low-moderate risk, liquid (toy label)"))
)
metta.space().add_atom(
E(S("risk_level"), S("growth_stocks"), ValueAtom("higher risk label in this toy graph"))
)
metta.space().add_atom(
E(
S("risk_level"),
S("cryptocurrency"),
ValueAtom("very high risk / volatility (toy label)"),
)
)
metta.space().add_atom(
E(
S("risk_level"),
S("savings_accounts"),
ValueAtom(
"often treated as low risk in teaching examples; "
"deposit insurance rules vary by country (US FDIC is one example only)"
),
)
)
metta.space().add_atom(
E(S("risk_level"), S("real_estate"), ValueAtom("moderate risk label (toy)"))
)
metta.space().add_atom(
E(S("risk_level"), S("options"), ValueAtom("very high risk / leveraged (toy label)"))
)

# Age → simplified teaching allocations only (not advice)
metta.space().add_atom(
E(
S("age_allocation"),
S("20s"),
ValueAtom("teaching example only: 80% stocks, 20% bonds"),
)
)
metta.space().add_atom(
E(
S("age_allocation"),
S("30s"),
ValueAtom("teaching example only: 70% stocks, 30% bonds"),
)
)
metta.space().add_atom(
E(
S("age_allocation"),
S("40s"),
ValueAtom("teaching example only: 60% stocks, 40% bonds"),
)
)
metta.space().add_atom(
E(
S("age_allocation"),
S("50s"),
ValueAtom("teaching example only: 50% stocks, 50% bonds"),
)
)
metta.space().add_atom(
E(
S("age_allocation"),
S("60s"),
ValueAtom("teaching example only: 40% stocks, 60% bonds"),
)
)

# Goals → strategies
metta.space().add_atom(
E(
S("goal_strategy"),
S("retirement"),
ValueAtom("toy: diversified index funds; retirement-account contributions"),
)
)
metta.space().add_atom(
E(
S("goal_strategy"),
S("emergency_fund"),
ValueAtom("toy: high-yield savings / money market style cash buffer"),
)
)
metta.space().add_atom(
E(
S("goal_strategy"),
S("house_down_payment"),
ValueAtom("toy: short-duration cash / short-term bonds"),
)
)
metta.space().add_atom(
E(
S("goal_strategy"),
S("wealth_building"),
ValueAtom("toy: growth-oriented diversified equity exposure"),
)
)
metta.space().add_atom(
E(
S("goal_strategy"),
S("passive_income"),
ValueAtom("toy: dividend stocks / bonds style examples"),
)
)

# Sector → illustrative example companies (not current top performers)
metta.space().add_atom(
E(
S("sector_stocks"),
S("technology"),
ValueAtom("illustrative names only: Apple, Microsoft, Google"),
)
)
metta.space().add_atom(
E(
S("sector_stocks"),
S("healthcare"),
ValueAtom("illustrative names only: Johnson & Johnson, Pfizer"),
)
)
metta.space().add_atom(
E(
S("sector_stocks"),
S("finance"),
ValueAtom("illustrative names only: JPMorgan Chase, Berkshire Hathaway"),
)
)
metta.space().add_atom(
E(
S("sector_stocks"),
S("energy"),
ValueAtom("illustrative names only: ExxonMobil, Chevron"),
)
)

# Common mistakes
metta.space().add_atom(
E(
S("mistake"),
S("timing_market"),
ValueAtom("toy warning: avoid trying to time peaks and valleys"),
)
)
metta.space().add_atom(
E(
S("mistake"),
S("lack_diversification"),
ValueAtom("toy warning: don't put all money in one stock or sector"),
)
)
metta.space().add_atom(
E(
S("mistake"),
S("emotional_trading"),
ValueAtom("toy warning: avoid panic selling or FOMO buying"),
)
)
metta.space().add_atom(
E(
S("mistake"),
S("high_fees"),
ValueAtom("toy warning: watch expensive product fees"),
)
)

# FAQ keys must match query_faq (not the raw user sentence)
metta.space().add_atom(
E(
S("faq"),
S("amount_to_invest"),
ValueAtom(
"Toy tip only: some teaching materials mention 10-20% of income "
"after an emergency fund. Not personalized advice."
),
)
)
metta.space().add_atom(
E(
S("faq"),
S("when_to_start"),
ValueAtom(
"Toy tip only: starting early is often cited for compounding demos. "
"Not personalized advice."
),
)
)
metta.space().add_atom(
E(
S("faq"),
S("diversification"),
ValueAtom(
"Toy definition: spreading exposure across assets to reduce "
"single-name risk. Not personalized advice."
),
)
)
metta.space().add_atom(
E(
S("faq"),
S("debt_first"),
ValueAtom(
"Toy tip only: high-interest debt is often prioritized before "
"investing in teaching examples. Not personalized advice."
),
)
)
metta.space().add_atom(
E(
S("faq"),
S("hi"),
ValueAtom("Hello! Ask about risk profiles, returns, allocation, or goals in this toy demo."),
)
)

Worked FAQ example: user says How much should I invest? → classifier keyword amount_to_invest → graph key amount_to_invest → seeded answer.

Expected keyword forms (normalize before MeTTa): conservative / moderate / aggressive; age buckets 20s60s (map “30-year-old” → 30s); goals retirement, emergency_fund; sectors technology; FAQ keys above.

Limitation: the classifier extracts one keyword. Sample queries below use a single focus term.

Step 2: Implement MeTTa lookup

Create metta/investment_rag.py:

import re
from hyperon import MeTTa, E, S, ValueAtom

SYMBOL_PATTERN = re.compile(r"^[a-z0-9_]+$")

# Map common natural-language fragments to seeded keys
KEYWORD_ALIASES = {
"30-year-old": "30s",
"30_year_old": "30s",
"thirty": "30s",
"20-year-old": "20s",
"40-year-old": "40s",
"50-year-old": "50s",
"60-year-old": "60s",
"emergency fund": "emergency_fund",
"down payment": "house_down_payment",
"how_much_should_i_invest": "amount_to_invest",
"when_should_i_start_investing": "when_to_start",
"what_is_diversification": "diversification",
"should_i_pay_off_debt_first": "debt_first",
}


def to_symbol(token: str):
"""Encode multi-word names; reject tokens that would break MeTTa."""
if token is None:
return None
raw = str(token).strip().strip('"').lower().replace("'", "").replace("\u2019", "")
if raw in KEYWORD_ALIASES:
return KEYWORD_ALIASES[raw]
symbol = raw.replace(" ", "_").replace("-", "_")
if symbol in KEYWORD_ALIASES:
return KEYWORD_ALIASES[symbol]
if not SYMBOL_PATTERN.fullmatch(symbol):
return None
return symbol


def atom_to_str(atom) -> str:
"""Parse both Symbol and ValueAtom results."""
try:
obj = atom.get_object()
if obj is not None and hasattr(obj, "value"):
return str(obj.value)
except Exception:
pass
return str(atom).strip('"')


class InvestmentRAG:
"""MeTTa knowledge lookup (not embedding / vector RAG)."""

def __init__(self, metta_instance: MeTTa):
self.metta = metta_instance

def _run_match(self, relation: str, subject: str):
symbol = to_symbol(subject)
if not symbol:
return []
query_str = f"!(match &self ({relation} {symbol} $x) $x)"
results = self.metta.run(query_str)
if not results:
return []
values = []
for row in results:
if row and len(row) > 0:
values.append(atom_to_str(row[0]))
return list(dict.fromkeys(values))

def query_risk_profile(self, risk_profile):
return self._run_match("risk_profile", risk_profile)

def get_expected_return(self, investment):
return self._run_match("expected_return", investment)

def get_risk_level(self, investment):
return self._run_match("risk_level", investment)

def get_age_allocation(self, age_group):
return self._run_match("age_allocation", age_group)

def get_goal_strategy(self, goal):
return self._run_match("goal_strategy", goal)

def query_sector_stocks(self, sector):
return self._run_match("sector_stocks", sector)

def get_mistake_warning(self, mistake):
return self._run_match("mistake", mistake)

def query_faq(self, question_or_key):
key = to_symbol(question_or_key)
if not key:
return None
results = self._run_match("faq", key)
return results[0] if results else None

def add_knowledge(self, relation_type, subject, object_value):
"""Same atom conventions as seed data. Used only when LEARN=1."""
rel = to_symbol(relation_type)
subj = to_symbol(subject)
if not rel or not subj or object_value is None:
return "Skipped invalid knowledge"

if rel == "risk_profile":
# One atom per investment type symbol (matches seed)
parts = [
to_symbol(p)
for p in str(object_value).replace(",", " ").split()
if to_symbol(p)
]
if not parts:
return "Skipped invalid investment symbols"
for obj in parts:
self.metta.space().add_atom(E(S(rel), S(subj), S(obj)))
return f"Added {rel}: {subj} -> {', '.join(parts)}"

atom_obj = ValueAtom(str(object_value))
self.metta.space().add_atom(E(S(rel), S(subj), atom_obj))
return f"Added {rel}: {subj} -> {object_value}"

Key Methods:

  • query_risk_profile(): Investment type symbols for a profile
  • get_expected_return() / get_risk_level(): Illustrative ValueAtom text
  • get_age_allocation() / get_goal_strategy(): Teaching examples
  • query_sector_stocks(): Illustrative company names (not live performance)
  • query_faq(): FAQ by stable key (amount_to_invest, hi), not the raw sentence
  • add_knowledge(): Same atom types as seed data (used only when LEARN=1)

Step 3: Query processing

Create metta/utils.py. Fallback if not prompt: sits at function scope after all intent branches. Default path does not write LLM output into the graph.

create_completion uses a smaller max_tokens (about 200) for short JSON intent classification and a larger budget (about 300–800) when humanizing a full reply — intent only needs a tiny JSON object; answers need room for the disclaimer and explanation.

import json
import os

from openai import OpenAI

from .investment_rag import InvestmentRAG, to_symbol

DISCLAIMER = (
"Not financial advice. This is a toy MeTTa demo for education only, "
"not personalized recommendations or a substitute for a licensed advisor."
)
LEARN = os.getenv("LEARN") == "1"


class LLM:
def __init__(self, api_key):
self.client = OpenAI(
api_key=api_key,
base_url="https://api.asi1.ai/v1",
)

def create_completion(self, prompt, max_tokens=800):
completion = self.client.chat.completions.create(
messages=[{"role": "user", "content": prompt}],
model="asi1",
max_tokens=max_tokens,
)
return completion.choices[0].message.content


def get_intent_and_keyword(query, llm):
"""Agent-side ASI:One call: classify intent and extract one keyword."""
prompt = (
f"Given the investment query: '{query}'\n"
"Classify the intent as one of: 'risk_profile', 'investment_advice', "
"'returns', 'allocation', 'goal', 'sector', 'mistake', 'faq', or 'unknown'.\n"
"Extract one keyword as a snake_case graph key.\n"
"Age phrases like '30-year-old' must become '30s' (also 20s/40s/50s/60s).\n"
"Goals: retirement, emergency_fund, house_down_payment, wealth_building, passive_income.\n"
"FAQs: amount_to_invest, when_to_start, diversification, debt_first; greetings → hi.\n"
"Risk profiles: conservative, moderate, aggressive.\n"
"Return *only* JSON:\n"
'{ "intent": "<classified_intent>", "keyword": "<extracted_keyword>" }'
)
response = llm.create_completion(prompt, max_tokens=200)
try:
cleaned = response.strip()
if cleaned.startswith("```"):
cleaned = "\n".join(cleaned.split("\n")[1:])
if cleaned.endswith("```"):
cleaned = "\n".join(cleaned.split("\n")[:-1])
result = json.loads(cleaned.strip())
return result["intent"], result.get("keyword")
except (json.JSONDecodeError, KeyError):
return "unknown", None


def generate_knowledge_response(query, intent, keyword, llm):
"""Optional LLM guess. Do not persist unless LEARN=1."""
if intent == "risk_profile":
prompt = (
f"Query: '{query}'\n"
f"Risk profile '{keyword}' is missing from the toy graph. Suggest 2-3 "
f"snake_case investment type tokens (e.g. bonds index_funds). Return only those tokens."
)
elif intent == "investment_advice":
prompt = (
f"Query: '{query}'\n"
f"No graph notes for '{keyword}'. Give a one-sentence educational overview "
f"(not personalized advice). Return only that text."
)
elif intent == "returns":
prompt = (
f"Query: '{query}'\n"
f"No illustrative return range for '{keyword}'. Suggest one short toy range "
f"labeled as illustrative only. Return only that text."
)
elif intent == "allocation":
prompt = (
f"Query: '{query}'\n"
f"No teaching allocation for '{keyword}'. Suggest one simplified stocks/bonds "
f"example labeled teaching-only. Return only that text."
)
elif intent == "goal":
prompt = (
f"Query: '{query}'\n"
f"No strategy for '{keyword}'. Suggest one short educational approach. Return only that text."
)
elif intent == "sector":
prompt = (
f"Query: '{query}'\n"
f"No illustrative names for '{keyword}'. Suggest a few example companies "
f"(not performance claims). Return only a short list."
)
elif intent == "mistake":
prompt = (
f"Query: '{query}'\n"
f"No warning for '{keyword}'. Give one short educational caution. Return only that text."
)
elif intent == "faq":
prompt = (
f"Query: '{query}'\n"
"Provide a concise educational overview and remind the user this is not financial advice. "
"Return only the answer."
)
else:
return None
return llm.create_completion(prompt, max_tokens=200)


def process_query(query, rag: InvestmentRAG, llm: LLM):
intent, keyword = get_intent_and_keyword(query, llm)
keyword = to_symbol(keyword) if keyword else None
prompt = ""

if intent == "faq":
faq_key = keyword or to_symbol(query)
faq_answer = rag.query_faq(faq_key) if faq_key else None
if faq_answer:
prompt = (
f"Query: '{query}'\n"
f"FAQ Answer: '{faq_answer}'\n"
"Humanize with a friendly educational tone. Keep not-financial-advice meaning."
)
else:
new_answer = generate_knowledge_response(query, intent, keyword, llm)
if LEARN and faq_key and new_answer:
rag.add_knowledge("faq", faq_key, new_answer)
prompt = (
f"Query: '{query}'\n"
f"FAQ Answer: '{new_answer}'\n"
"Humanize with a friendly educational tone. This is not financial advice."
)
elif intent == "risk_profile" and keyword:
investments = rag.query_risk_profile(keyword)
if not investments:
investment_types = generate_knowledge_response(query, intent, keyword, llm)
if LEARN and investment_types:
rag.add_knowledge("risk_profile", keyword, investment_types)
prompt = (
f"Query: '{query}'\n"
f"Risk Profile: {keyword}\n"
f"Suitable Investments (unverified LLM suggestion, not a graph fact): {investment_types}\n"
"Be explicit this is a toy educational overview, not recommendations."
)
else:
investment_details = []
for investment in investments:
returns = rag.get_expected_return(investment)
risks = rag.get_risk_level(investment)
investment_details.append(
{
"type": investment,
"returns": returns[0] if returns else "N/A",
"risks": risks[0] if risks else "N/A",
}
)
prompt = (
f"Query: '{query}'\n"
f"Risk Profile: {keyword}\n"
f"Investment Options (toy graph): {investment_details}\n"
"Give an educational overview only. Label returns as illustrative ranges."
)
elif intent == "returns" and keyword:
returns = rag.get_expected_return(keyword)
risks = rag.get_risk_level(keyword)
if not returns:
return_info = generate_knowledge_response(query, intent, keyword, llm)
if LEARN and return_info:
rag.add_knowledge("expected_return", keyword, return_info)
prompt = (
f"Query: '{query}'\n"
f"Investment: {keyword}\n"
f"Expected Returns (unverified LLM suggestion): {return_info}\n"
"Say this is not a forecast and not from the verified toy graph."
)
else:
prompt = (
f"Query: '{query}'\n"
f"Investment: {keyword}\n"
f"Illustrative return ranges (toy graph, not forecasts): {', '.join(returns)}\n"
f"Risk notes: {', '.join(risks) if risks else 'Not specified'}\n"
"Explain as educational ranges only."
)
elif intent == "allocation" and keyword:
allocation = rag.get_age_allocation(keyword)
if not allocation:
allocation_advice = generate_knowledge_response(query, intent, keyword, llm)
if LEARN and allocation_advice:
rag.add_knowledge("age_allocation", keyword, allocation_advice)
prompt = (
f"Query: '{query}'\n"
f"Age Group: {keyword}\n"
f"Allocation (unverified LLM suggestion): {allocation_advice}\n"
"Label as a simplified teaching example only."
)
else:
prompt = (
f"Query: '{query}'\n"
f"Age Group: {keyword}\n"
f"Teaching allocation example: {', '.join(allocation)}\n"
"Explain as a simplified classroom example, not advice."
)
elif intent == "goal" and keyword:
strategies = rag.get_goal_strategy(keyword)
if not strategies:
strategy = generate_knowledge_response(query, intent, keyword, llm)
if LEARN and strategy:
rag.add_knowledge("goal_strategy", keyword, strategy)
prompt = (
f"Query: '{query}'\n"
f"Investment Goal: {keyword}\n"
f"Strategy (unverified LLM suggestion): {strategy}\n"
"Educational overview only."
)
else:
prompt = (
f"Query: '{query}'\n"
f"Investment Goal: {keyword}\n"
f"Toy strategies: {', '.join(strategies)}\n"
"Educational overview only."
)
elif intent == "sector" and keyword:
stocks = rag.query_sector_stocks(keyword)
if not stocks:
sector_info = generate_knowledge_response(query, intent, keyword, llm)
if not sector_info:
sector_info = (
"No specific names generated; consider diversified sector funds in real life "
"(this demo is not advice)."
)
elif LEARN:
rag.add_knowledge("sector_stocks", keyword, sector_info)
prompt = (
f"Query: '{query}'\n"
f"Sector: {keyword}\n"
f"Illustrative names: {sector_info}\n"
"Do not claim current performance rankings."
)
else:
prompt = (
f"Query: '{query}'\n"
f"Sector: {keyword}\n"
f"Illustrative example companies (not top performers / not recommendations): "
f"{', '.join(stocks)}\n"
"Educational naming only."
)
elif intent == "investment_advice" and keyword:
exp = rag.get_expected_return(keyword)
risks = rag.get_risk_level(keyword)
if exp or risks:
prompt = (
f"Query: '{query}'\n"
f"Investment type: {keyword}\n"
f"Illustrative returns (toy graph): {', '.join(exp) if exp else 'N/A'}\n"
f"Risk notes (toy graph): {', '.join(risks) if risks else 'N/A'}\n"
"Educational overview only; not a recommendation."
)
else:
advice = generate_knowledge_response(query, intent, keyword, llm)
if not advice:
advice = (
"Consider diversified, low-cost options aligned with your horizon in real life; "
"consult a licensed professional for personal advice."
)
# investment_advice does not invent new graph edges unless LEARN=1
# and you choose to store expected_return text for consistency
if LEARN and advice:
rag.add_knowledge("expected_return", keyword, advice)
prompt = (
f"Query: '{query}'\n"
f"Investment type: {keyword}\n"
f"Guidance: {advice}\n"
"Educational overview only; not a recommendation."
)
elif intent == "mistake" and keyword:
warnings = rag.get_mistake_warning(keyword)
if warnings:
wtext = ", ".join(warnings)
else:
wtext = generate_knowledge_response(query, intent, keyword, llm)
if not wtext:
wtext = (
"Prioritize diversification, discipline, and awareness of fees and emotions "
"(toy teaching note)."
)
elif LEARN:
rag.add_knowledge("mistake", keyword, wtext)
prompt = (
f"Query: '{query}'\n"
f"Topic / behavior: {keyword}\n"
f"Warning(s): {wtext}\n"
"Explain clearly as educational caution, not personalized advice."
)

if not prompt:
prompt = (
f"Query: '{query}'\n"
"No specific info found in the toy graph. Offer general educational assistance "
"and suggest consulting a licensed financial professional."
)

prompt += (
f"\nAlways start the answer with: {DISCLAIMER}\n"
"Then give the helpful educational content. Do not invent personalized buy/sell orders."
)
response = llm.create_completion(prompt, max_tokens=800)
text = (response or "").strip()
if DISCLAIMER.lower() not in text.lower():
text = f"{DISCLAIMER}\n\n{text}"
return {"selected_question": query, "humanized_answer": text}

Intent Classification (runs in the agent, after Chat Protocol receives text):

  • risk_profile: keyword → investment type symbols in the toy graph
  • returns / investment_advice: illustrative return / risk notes
  • allocation: age keys 20s60s (teaching examples)
  • goal / sector / mistake: keyed strategies, illustrative names, warnings
  • faq: keyed FAQs (amount_to_invest, when_to_start, diversification, debt_first, hi)

Step 4: Configure Agent

Create agent.py at the project root (not inside metta/):

from datetime import datetime, timezone
from uuid import uuid4
import os
import sys

from dotenv import load_dotenv
from uagents import Context, Protocol, Agent
from hyperon import MeTTa

from uagents_core.contrib.protocols.chat import (
ChatAcknowledgement,
ChatMessage,
EndSessionContent,
StartSessionContent,
TextContent,
chat_protocol_spec,
)

from metta.investment_rag import InvestmentRAG
from metta.knowledge import initialize_investment_knowledge
from metta.utils import LLM, process_query, DISCLAIMER

load_dotenv()

api_key = os.getenv("ASI_ONE_API_KEY")
agent_seed = os.getenv("AGENT_SEED")

if not api_key:
print("Missing ASI_ONE_API_KEY. Create a key at https://asi1.ai/dashboard/api-keys and put it in .env")
sys.exit(1)

if not agent_seed:
print("Missing AGENT_SEED. Set a unique local seed in .env (do not commit secrets).")
sys.exit(1)

agent = Agent(
name="Financial Investment Advisor",
seed=agent_seed,
port=8008,
mailbox=True,
publish_agent_details=True,
)


def create_text_chat(text: str, end_session: bool = False) -> ChatMessage:
content = [TextContent(type="text", text=text)]
if end_session:
content.append(EndSessionContent(type="end-session"))
return ChatMessage(
timestamp=datetime.now(timezone.utc),
msg_id=uuid4(),
content=content,
)


metta = MeTTa()
initialize_investment_knowledge(metta)
rag = InvestmentRAG(metta)
llm = LLM(api_key=api_key)

chat_proto = Protocol(spec=chat_protocol_spec)


@chat_proto.on_message(ChatMessage)
async def handle_message(ctx: Context, sender: str, msg: ChatMessage):
ctx.storage.set(str(ctx.session), sender)
await ctx.send(
sender,
ChatAcknowledgement(
timestamp=datetime.now(timezone.utc),
acknowledged_msg_id=msg.msg_id,
),
)

for item in msg.content:
if isinstance(item, StartSessionContent):
ctx.logger.info(f"Got a start session message from {sender}")
continue
elif isinstance(item, TextContent):
user_query = item.text.strip()
ctx.logger.info(f"Got an investment query from {sender}: {user_query}")
try:
response = process_query(user_query, rag, llm)
answer_text = response.get(
"humanized_answer",
f"{DISCLAIMER}\n\nI could not process your query.",
)
await ctx.send(sender, create_text_chat(answer_text))
except Exception as e:
ctx.logger.error(f"Error processing investment query: {e}")
await ctx.send(
sender,
create_text_chat(
f"{DISCLAIMER}\n\nI hit an error processing that query. Please try again."
),
)
else:
ctx.logger.info(f"Got unexpected content from {sender}")


@chat_proto.on_message(ChatAcknowledgement)
async def handle_ack(ctx: Context, sender: str, msg: ChatAcknowledgement):
ctx.logger.info(
f"Got an acknowledgement from {sender} for {msg.acknowledged_msg_id}"
)


agent.include(chat_proto, publish_manifest=True)

if __name__ == "__main__":
agent.run()

Agent Features:

  • Toy MeTTa lookup only — not a financial advisor product or personalized advice
  • Every reply includes a not financial advice disclaimer
  • Does not persist unverified LLM output into the graph unless LEARN=1
  • Agent LLM (ASI:One) classifies intent after Chat Protocol receives text
  • Compatible with ASI:One via Chat Protocol and Agentverse mailbox (mailbox=True)

Detailed Working (Step-by-Step)

  1. User sends a query through ASI:One chat (or Inspector chat).
  2. Chat Protocol handler receives TextContent.
  3. The agent calls ASI:One (get_intent_and_keyword) to classify intent and one keyword.
  4. InvestmentRAG runs MeTTa match queries on the toy graph (knowledge lookup, not vector RAG).
  5. Reply is humanized, disclaimer is prepended, and Chat Protocol sends a string (not a dict).
  6. Graph writes from LLM guesses happen only when LEARN=1.

Testing and Deployment

Local Testing (mailbox)

Numbered steps matching current uAgents + Agentverse. See also Mailbox agents and uAgent creation.

  1. Log in to Agentverse.

  2. From project/, with venv active and .env set:

    python3 agent.py

    Windows: py -3.10 agent.py.

  3. In the console, copy the inspector URL (it includes your agent address). Expected lines look like:

    INFO:     [Financial Investment Advisor]: Starting agent with address: agent1q...
    INFO: [Financial Investment Advisor]: Agent inspector available at https://agentverse.ai/inspect/?uri=http%3A//127.0.0.1%3A8008&address=agent1q...
    INFO: [Financial Investment Advisor]: Starting mailbox client for https://agentverse.ai
    INFO: [mailbox]: Successfully registered as mailbox agent in Agentverse

    If you see Missing ASI_ONE_API_KEY, stop and fix .env — the agent exits before agent.run().

  4. Open the inspector URL while logged in. Choose ConnectMailbox (Agentverse issues the mailbox token; you do not paste Python import mailbox).

  5. Use Chat with Agent on the Inspector/profile, or continue to ASI:One below. Keep agent.py running.

Sample queries (aligned with seeded keys)

  • Hi → FAQ key hi (greeting)
  • How much should I invest? → FAQ key amount_to_invest (seeded FAQ)
  • I am a conservative investor, what should I invest in? → risk profile conservativebonds, dividend_stocks, savings_accounts
  • What returns can I expect from index funds? → illustrative ranges for index_funds
  • How should a 30-year-old allocate a portfolio? → age key 30s (teaching example)
  • What strategy works best for retirement? → goal retirement
  • What are common investing mistakes to avoid? → mistake keys such as timing_market / lack_diversification (one keyword per turn)

Query your agent from ASI:One

ASI:One discovers mailbox agents that are running, registered, and using Chat Protocol. README/handle tips: Searching agents. Chat UI: ASI:One Chat.

  1. Copy the agent address from the console (agent1q...). Optionally set a handle on the Agentverse profile.
  2. Open ASI:One, sign in with Google or the ASI:One wallet, and start a new chat.
  3. Toggle Agents so ASI:One can call Agentverse agents.
  4. Paste the address or @handle and send a sample query such as I am a conservative investor, what should I invest in?
  5. Expect a reply that starts with the not-financial-advice disclaimer and mentions toy graph options such as bonds / dividend_stocks / savings_accounts. The local console should log the incoming chat message.

Expected output

Startup (shape of logs; address is unique to your AGENT_SEED):

INFO:     [Financial Investment Advisor]: Starting agent with address: agent1q...
INFO: [Financial Investment Advisor]: Agent inspector available at https://agentverse.ai/inspect/?uri=...
INFO: [mailbox]: Successfully registered as mailbox agent in Agentverse

Example chat:

  • You: How much should I invest?

  • Agent: Starts with Not financial advice... then, from the toy FAQ key amount_to_invest, gives the seeded educational tip (not a personalized plan).

  • You: I am a conservative investor, what should I invest in?

  • Agent: Starts with the disclaimer, then lists toy-graph options like bonds, dividend_stocks, and savings_accounts with illustrative return/risk notes.

Notes

  • Educational toy demo only — not personalized financial advice.
  • Default path does not persist unverified LLM financial “facts”; set LEARN=1 only for local experiments.
  • For production usage, add compliance rules, disclosure controls, and policy guardrails.
  • Sibling MeTTa samples: Medical Agent with MeTTa, Fetch.ai Knowledge Assistant with MeTTa.