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
uagents0.25.x). On Windows usepy -3.10; on WSL/macOS/Linux usepython3. - 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
Option 1: Install All Dependencies at Once (Recommended)
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:
- Install WSL and Ubuntu.
- Inside WSL: install Python 3.10+, create the venv above, then
pip install -r requirements.txt. - Run
python3 agent.pyfrom 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

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
agent.py: Main uAgent with Chat Protocolmetta/knowledge.py: Toy MeTTa graph (illustrative US-centric teaching data — not market facts)metta/investment_rag.py: MeTTa lookup helpersmetta/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 20s–60s (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 profileget_expected_return()/get_risk_level(): Illustrative ValueAtom textget_age_allocation()/get_goal_strategy(): Teaching examplesquery_sector_stocks(): Illustrative company names (not live performance)query_faq(): FAQ by stable key (amount_to_invest,hi), not the raw sentenceadd_knowledge(): Same atom types as seed data (used only whenLEARN=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
20s…60s(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)
- User sends a query through ASI:One chat (or Inspector chat).
- Chat Protocol handler receives
TextContent. - The agent calls ASI:One (
get_intent_and_keyword) to classify intent and one keyword. InvestmentRAGruns MeTTamatchqueries on the toy graph (knowledge lookup, not vector RAG).- Reply is humanized, disclaimer is prepended, and Chat Protocol sends a string (not a dict).
- 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.
-
Log in to Agentverse.
-
From
project/, with venv active and.envset:python3 agent.pyWindows:
py -3.10 agent.py. -
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 AgentverseIf you see
Missing ASI_ONE_API_KEY, stop and fix.env— the agent exits beforeagent.run(). -
Open the inspector URL while logged in. Choose Connect → Mailbox (Agentverse issues the mailbox token; you do not paste Python
import mailbox). -
Use Chat with Agent on the Inspector/profile, or continue to ASI:One below. Keep
agent.pyrunning.
Sample queries (aligned with seeded keys)
Hi→ FAQ keyhi(greeting)How much should I invest?→ FAQ keyamount_to_invest(seeded FAQ)I am a conservative investor, what should I invest in?→ risk profileconservative→bonds,dividend_stocks,savings_accountsWhat returns can I expect from index funds?→ illustrative ranges forindex_fundsHow should a 30-year-old allocate a portfolio?→ age key30s(teaching example)What strategy works best for retirement?→ goalretirementWhat are common investing mistakes to avoid?→ mistake keys such astiming_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.
- Copy the agent address from the console (
agent1q...). Optionally set a handle on the Agentverse profile. - Open ASI:One, sign in with Google or the ASI:One wallet, and start a new chat.
- Toggle Agents so ASI:One can call Agentverse agents.
- Paste the address or
@handleand send a sample query such asI am a conservative investor, what should I invest in? - 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, andsavings_accountswith illustrative return/risk notes.
Notes
- Educational toy demo only — not personalized financial advice.
- Default path does not persist unverified LLM financial “facts”; set
LEARN=1only 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.