Investment Scanner Agent Jev
FreeNot checkedEducational example to learn Jev concepts (TypeSafe.ai) with an AI Agent.
About
Educational example to learn Jev concepts (TypeSafe.ai) with an AI Agent.
README
An educational example agent built with LangChain, LangGraph, a local Ollama model, and one MCP tool. It scans news and price data for a narrow scope: equities (stocks) and ETFs only. It is a news/trend summarizer, not a financial advisor -- it never recommends buying or selling anything.
Which code am I looking at? This is the Jev-extended version of the project, covered in Video 3 -- it adds a fourth tool,
assess_trading_stance, on top of everything from Videos 1 and 2. If you want the exact code from an earlier video, those are separate sibling projects, not branches or tags of this one -- see Code versions per video below.
Project structure
investment-scanner-agent-jev/
├── README.md
├── NOTES.md
├── LICENSE
├── .gitignore
├── requirements.txt
├── config.ini # runtime settings (model, cache, RAG backend, Jev mode)
├── run.py # entry point / demo script (hardcoded questions)
├── app.py # Streamlit UI (question box -> answer box)
├── market_log.md # generated on first run -- the news cache
├── rag_cache/ # generated on first run -- embeddings / vector store (gitignored)
└── investment_scanner/ # the package
├── __init__.py # exposes build_agent(), ask()
├── agent.py # the agent: tools, system prompt, create_agent
│ # (LangGraph rebuild commented at the bottom)
├── config.py # loads config.ini, with built-in defaults
├── mcp_server.py # standalone MCP server (equity/ETF price tool)
├── rag.py # RAG: glossary retrieval, two swappable backends
├── jev.py # Jev: structured-decision tool, live or simulated
└── knowledge/
└── glossary.md # the curated equities/ETF glossary RAG retrieves from
investment_scanner/agent.py-- the agent, built with LangChain'screate_agent()on top of a local Ollama model. A commented-out LangGraph version of the exact same agent is at the bottom of the file -- uncomment it once you're ready to move pastcreate_agentand control the graph yourself.investment_scanner/config.py-- readsconfig.inifrom the repo root (ollama_model,log_path,freshness_hours, the[rag]settings, and the[jev]settings) and exposes them as constants. Falls back to the project's built-in defaults if the file, or a given key in it, is missing, so the project still runs out of the box with noconfig.iniat all.investment_scanner/mcp_server.py-- your MCP use case: a standalone server exposing one tool,get_stock_snapshot, backed by free Yahoo Finance data viayfinance. The agent talks to this as a separate process over stdio, the same way it would talk to any third-party MCP server.investment_scanner/rag.py-- Retrieval-Augmented Generation overinvestment_scanner/knowledge/glossary.md, behind a third tool,explain_market_term. See Retrieval-Augmented Generation (RAG): a third tool below.investment_scanner/jev.py-- turns text the agent already gathered into a typed, calibrated decision using Jev (TypeSafe's structured-decision model), behind a fourth tool,assess_trading_stance. See Jev: a fourth tool, and the one that isn't free below -- this is the one piece of the whole project that costs money, so it defaults to a simulated response.config.ini-- edit this to change the Ollama model, cache behavior, RAG backend, or Jev mode without touching code (see "Configuration" below).run.py-- the script you actually run; imports the package and asks it a few demo questions.app.py-- a one-page Streamlit UI: type a question in a text box, hit Ask, see the agent's answer in a text box below it, plus banners and metrics when the cache, RAG, or Jev tools fired. Uses the samebuild_agent()/ask()API asrun.py.
Why (almost) everything here is free
- Ollama runs the chat model and the embedding model locally on your own machine -- no API key, no per-token billing. The only cost is your own hardware/electricity.
- ddgs (DuckDuckGo search) is a keyless, free news search library.
- yfinance pulls from Yahoo Finance's public endpoints for free, no account needed.
- Chroma (the optional RAG backend) is an embedded, fully local vector database -- no server to run, no account, no cloud calls.
- Jev is the one exception -- see the next section before you change
config.ini's[jev]mode.
Hardware
- RAM: the default model,
llama3.1(8B), needs roughly 8 GB RAM to run at all; 16 GB is a more comfortable baseline if you're also running a browser, IDE, or Streamlit alongside it. Skip the 70B/405Bllama3.1variants unless you have serious hardware (64GB+ RAM or a strong GPU) -- this project assumes the 8B default. The embedding model used for RAG,nomic-embed-text, is tiny by comparison (under 1 GB) and doesn't meaningfully add to this.assess_trading_stanceadds no local hardware requirement at all -- in its default simulated mode it's pure Python; in live mode it's a network call, not something that runs on your machine. - GPU: not required. Ollama runs 8B models fine on CPU, just slower per response; Apple Silicon Macs get solid performance automatically via Metal, and an NVIDIA/CUDA GPU helps on other machines but isn't needed.
- Disk: a few GB free for the model pulls, plus normal space for the Python venv and dependencies.
- Network: needed for setup (
pip install,ollama pull) and at runtime for the two live-data tools --scan_market_news(DuckDuckGo) andget_stock_snapshot(Yahoo Finance) -- unless a cached hit avoids it, plusassess_trading_stanceif you've switched it to live mode.explain_market_termand model inference are fully offline once the models are pulled and the glossary has been embedded once.
One-time setup
- Install Python 3.10+ if you don't already have it -- download from
python.org, or on a Mac:
Check what you have withbrew install pythonpython3 --version. - Install Ollama if you haven't already -- download from
ollama.com, or on a Mac:
Make sure it's actually running afterward (the Ollama app, orbrew install ollamaollama servein a terminal). - Pull a tool-calling-capable model (required -- not every Ollama model
supports tool calls):
This downloads several GB, so it'll take a few minutes depending on your connection.ollama pull llama3.1qwen2.5andmistral-nemoare alternatives that also support tool calling, if you want to compare. - Pull the embedding model used by the
explain_market_termRAG tool (small, usually well under a minute):ollama pull nomic-embed-text - Open a terminal in the project folder and set up Python:
cd ~/vsc_projects/investment-scanner-agent-jev python3 -m venv .venv source .venv/bin/activate # on Windows: .venv\Scripts\activate pip install -r requirements.txtchromadbinstalls by default but is only imported if you switchconfig.ini's RAG backend tochroma(see below) -- the defaultnumpybackend doesn't need it. - Optional, and only if you want live Jev calls instead of the
simulated default: get an API key from
console.typesafe.ai (Jev is in early
access at the time of writing) and export it before running anything:
Skip this entirely to use the project as shipped -- see Jev: a fourth tool, and the one that isn't free.export TYPESAFE_API_KEY=your-key-here
Run it
python run.py
This runs a few example questions -- a news scan, a price snapshot, and a
structured trading-stance assessment -- and prints the agent's answers to
the terminal. Edit the questions list in run.py to try your own.
Or run the UI
streamlit run app.py
This opens a one-page app in your browser with a question box and an answer box -- type any question and click Ask. Try something like "Get a snapshot of AAPL and give me a structured trading stance on it" to see the Jev banner and metrics appear. The agent is built once (the first click will be slower while Ollama and the MCP server start up, and the glossary gets embedded for the first time) and reused for the rest of the session, with each question/answer sharing the same conversation memory.
Configuration
config.ini (repo root) controls runtime settings without touching code:
[agent]
ollama_model = llama3.1
[cache]
log_path = market_log.md
freshness_hours = 4
[rag]
backend = numpy
embedding_model = nomic-embed-text
top_k = 3
[jev]
mode = auto
model = jev-latest
ollama_model-- which pulled Ollama model the agent uses. Must be tool-calling-capable (see "One-time setup" above).log_path-- where the news cache file lives, relative to the repo root.freshness_hours-- how long a cachedscan_market_newsresult stays fresh before it's treated as stale and re-searched live.backend--numpy(plain cosine similarity, cached to two small files underrag_cache/) orchroma(a real local vector database). Both answer identically; see the RAG section below.embedding_model-- the free local Ollama model used to embed both the glossary and each query. Must be pulled first (see "One-time setup").top_k-- how many glossary entriesexplain_market_termretrieves per lookup.mode(Jev) --auto(default: live ifTYPESAFE_API_KEYis set, simulated otherwise),simulated(always simulate), orlive(always call the real API, erroring if no key is set). See the Jev section below.model(Jev) -- which model id to request from the real API, e.g.jev-latestor a pinned version likejev-1.13. Unused in simulated mode.
investment_scanner/config.py reads this file and falls back to the
defaults above for the file itself, or any individual key, if missing --
so the project runs fine with no config.ini at all.
How the pieces fit together
build_agent()(ininvestment_scanner/agent.py) starts aChatOllamamodel pointed at your local model.- It launches
investment_scanner/mcp_server.pyas a subprocess viaMultiServerMCPClientand pulls its tool(s) in withget_tools(). - Those MCP tools are combined with the locally-defined
scan_market_news,explain_market_term, andassess_trading_stancetools into one list and handed tocreate_agent()along with a system prompt that scopes the agent to equities/ETFs and bans investment advice. create_agent()builds a LangGraph graph behind the scenes (a loop of model -> tool call -> tool -> model, repeating until a final answer), though you never touch that graph directly in this version.MemorySavergives the agent per-thread_idmemory, so callingask()twice with the samethread_idlets it recall the earlier turn.- The first time
explain_market_termis called,rag.pyembedsinvestment_scanner/knowledge/glossary.mdonce and caches the result underrag_cache/-- every call after that (including in later runs) reuses the cache instead of re-embedding. - When the model calls
assess_trading_stance,jev.pydecides whether to call the real Jev API or return a deterministic simulated response (see the next section), and the tool hands back both a human-readable summary and a small embedded JSON blob thatask()extracts for the UI.
The news cache: market_log.md
scan_market_news checks market_log.md (created at the repo root on
first run) before searching. If it finds an entry for the same query
newer than CACHE_FRESHNESS_HOURS (4 hours by default, set in
config.ini -- see "Configuration" above), it returns that instead of
making a new network call;
otherwise it searches live and appends the result as a new dated entry.
This is deliberately separate from MemorySaver: MemorySaver remembers
a conversation in RAM for as long as the process is running;
market_log.md remembers search results on disk, across separate runs,
until they go stale. get_stock_snapshot is intentionally NOT cached the
same way -- a 4-hour-old price is just wrong, unlike a 4-hour-old
headline. Matching is exact (case/whitespace-normalized) text, so
differently-phrased repeat questions still count as new searches -- a
known, accepted limitation, not a bug.
Retrieval-Augmented Generation (RAG): a third tool
explain_market_term answers "what does X mean" questions (P/E ratio,
market cap, ETF vs mutual fund, ...) from a small curated glossary
(investment_scanner/knowledge/glossary.md) instead of the model's own
training data or a live web search. See NOTES.md for why this is framed
as a Tool + long-term Memory rather than a new, separate concept -- the
short version is: RAG isn't a fifth ingredient alongside Model, Tools,
Memory, and Loop, it's a specific, very common way of building a Tool,
backed by a specific flavor of long-term Memory.
How it works, end to end:
glossary.mdis split into one chunk per## Termheading (_load_chunks()inrag.py).- Each chunk is embedded once (turned into a vector of numbers that
captures its meaning) by a free local Ollama model,
nomic-embed-text. - When the agent calls
explain_market_term(query), the query itself gets embedded the same way, and the chunks whose vectors are most similar to the query's vector (by cosine similarity) are returned as plain text. - The model then writes its answer using that retrieved text, instead of guessing from training data that might be outdated, wrong, or simply not specific to this project's glossary wording.
Two interchangeable backends, chosen by config.ini's [rag]
backend setting:
numpy(default) -- the cosine similarity math is written out by hand in_cosine_similarity()(about 5 lines), and the embeddings are cached to two plain files underrag_cache/(kb_embeddings.npy,kb_chunks.json). No extra dependency. This is deliberately the least "magic" way to do it -- a vector database's search step is this formula; bigger stores just add an approximate-nearest-neighbor index on top so it stays fast at millions of rows instead of the 8 we have here.chroma-- the same idea via Chroma, a real embedded vector database (pip install chromadb, already inrequirements.txt), persisted underrag_cache/chroma_db/. Closer to what you'd reach for in a larger project with a bigger, changing knowledge base.
Both backends implement the same search(query, k) -> list[str]
interface and use the same embedding model, so switching backend in
config.ini changes nothing about what the agent can answer -- only how
the lookup happens underneath. This mirrors the create_agent() /
hand-written-LangGraph swap already in agent.py: same behavior, two ways
to implement it, pick based on how much control you want.
Extending it: to add more glossary terms, add more ## Term sections
to glossary.md and delete rag_cache/ (or its chroma_db/ subfolder)
so it re-embeds on next run -- the code doesn't need to change. To point
RAG at a completely different knowledge base (say, a folder of your own
notes or filings), swap out _load_chunks()'s source file for something
that walks a folder instead.
Jev: a fourth tool, and the one that isn't free
assess_trading_stance turns a price snapshot and/or recent headlines the
agent has already gathered into a typed, calibrated decision using
Jev, a "System One" model from TypeSafe AI
released in very-early access. Instead of writing more prose, Jev takes a
block of text (the "state") plus one or more typed questions and returns
calibrated numbers:
- Noul -- a probability (0-1) that a yes/no statement is true. Used here to ask: is there a high probability of positive near-term momentum, based only on the state text?
- Score -- a probability-weighted rating on a scale you define. Used here to rate how stretched the valuation looks, on a 5-level scale from "Deep value" to "Severely overvalued".
- Choice -- a pick-one classification with a probability distribution and a confidence value. Used here to classify a Strong Buy / Buy / Hold / Sell / Strong Sell "stance" label.
How it's wired into this agent: assess_trading_stance does not
fetch any data itself -- the model is instructed (see the system prompt in
agent.py) to call get_stock_snapshot and/or scan_market_news first,
summarize what it found as context, and only then call
assess_trading_stance(ticker, context). That summary plus the ticker
becomes Jev's "state" text; the three questions above are fixed in
jev.py's _questions_for_stance() so the response shape (and the
Streamlit UI built on top of it) is always predictable.
Why this defaults to a simulated response. Every other piece of this
project -- Ollama, ddgs, yfinance, Chroma -- is free and local. Jev is not:
it's a hosted, early-access, paid API (TypeSafe's list price is
$0.042 per million input tokens with free output, so a whole demo session
costs a small fraction of a cent -- but it does require an account and a
key from console.typesafe.ai, not a
pip install and a pull). To keep this project's "clone it and run it,
no API key" promise intact, config.ini's [jev] mode defaults to
auto, which means:
- No
TYPESAFE_API_KEYset ->assess_trading_stancereturns a deterministic simulated response, shaped exactly like a real Jev answer (jev.py's_simulate()), lightly informed by any P/E figure it finds in the state text so the numbers aren't obviously disconnected from what the agent actually looked up. It's a stand-in for the real model, not a prediction -- treat it as a UI/shape demo, not a signal. TYPESAFE_API_KEYset -> real calls tohttps://api.typesafe.ai/v1/systemone.
Set mode = simulated or mode = live in config.ini to force one or the
other regardless of whether a key is set (handy for a repeatable
recording, or to fail loudly if a key is missing rather than silently
falling back).
A note on getting API access: at the time of writing, several
third-party sites showed up in search results advertising "free Jev API
keys, no waitlist" -- these are not TypeSafe's own domains and look like
lead-gen pages riding the launch's popularity rather than an official
program. Get a key directly from console.typesafe.ai if you want to try
live calls, and treat unaffiliated "free key" offers with the same
skepticism you'd apply to any other unfamiliar site asking for sign-up
details.
How this differs from RAG (the other "third tool," in the sibling
-rag project): RAG retrieves grounding text for the model to reason
over and write prose from -- it's a Tool backed by a flavor of long-term
Memory. Jev does the opposite: it takes text already gathered and turns it
into typed numbers, with no retrieval step and no prose of its own. Both
are still just Tools in the Model + Tools + Memory + Loop sense -- see
NOTES.md's section on adding Jev for the full "remove it and it's still
an agent" argument.
Moving to LangGraph
The bottom of investment_scanner/agent.py has a fully commented-out
LangGraph rebuild of the same agent (build_langgraph_agent, using
StateGraph, ToolNode, and tools_condition directly). It's commented
out so you have a working create_agent copy first. When you're ready to
add things create_agent doesn't support out of the box -- custom routing
between specialized sub-agents, a human-approval step before certain tool
calls, deterministic non-LLM steps mixed into the flow -- uncomment that
section (and stop calling build_agent) and build from there.
Code versions per video
This project is built up incrementally across a video series. Rather than
one repo whose main branch keeps changing underneath old video links,
each stage of the build is its own separate project folder/repo --
old links keep pointing at exactly the code they always did, and this
repo's own main is free to keep moving forward for whatever comes next.
| Video | What it covers | Project |
|---|---|---|
| 1 | Agent concepts (Model + Tools + Memory + Loop), code walkthrough, live demo | investment-scanner-agent |
| 2 | Adding Retrieval-Augmented Generation (explain_market_term) |
investment-scanner-agent-rag |
| 3 | Adding Jev structured decisions (assess_trading_stance) -- this repo |
investment-scanner-agent-jev |
If a future video adds another capability, it gets its own new sibling
project (e.g. investment-scanner-agent-<whatever>) rather than folding
into this one -- so this table just grows a row, and nothing already
published has to change underneath anyone.
Known rough edges (this is a teaching example, not production code)
- No retry logic if Ollama, DuckDuckGo, Yahoo Finance, or (in live mode) Jev are briefly unreachable.
yfinancescrapes public Yahoo Finance endpoints, which occasionally change shape or rate-limit -- ifget_stock_snapshotstarts failing, that's the most likely cause, not a bug in the MCP wiring.- No evaluation harness, logging, or alerting.
- Not every Ollama model supports tool calling; if the agent seems to
ignore your tools entirely, double check
OLLAMA_MODELinagent.pyis set to one that does. - The
market_log.mdcache matches queries by exact normalized text (case/whitespace-insensitive only) -- asking the same thing in differently-worded ways still counts as separate cache entries and triggers separate searches. It also only grows; nothing prunes old entries from the file over time. - The RAG cache's "has the glossary changed?" check is just a chunk-count
comparison, not a content hash -- if you edit an existing term's text
without adding or removing a
## Termheading, deleterag_cache/manually to force a re-embed. Fine for a small hand-edited glossary; not how you'd do this for a knowledge base that changes often. - Jev is in early access as of this writing -- the real API's request/
response shape in
jev.pywas built from TypeSafe's public docs and may drift as the product matures; if live calls start failing with 422s, checkdocs.typesafe.ai/apifor changes first. - The simulated Jev response is a naive, hand-written stand-in (a hashed seed plus a regex-extracted P/E figure) -- it demonstrates the shape of a Jev response, not anything resembling real market judgment. Never present its numbers, live or simulated, as investment advice.
License
MIT -- see LICENSE. Change the copyright holder there if you'd like.
Installing Investment Scanner Agent Jev
This server has no published package — it is built from source. Open the repository and follow its README.
▸ github.com/mkgraiitr/investment-scanner-agent-jevFAQ
Is Investment Scanner Agent Jev MCP free?
Yes, Investment Scanner Agent Jev MCP is free — one-click install via Unyly at no cost.
Does Investment Scanner Agent Jev need an API key?
No, Investment Scanner Agent Jev runs without API keys or environment variables.
Is Investment Scanner Agent Jev hosted or self-hosted?
Self-hosted: the server runs locally on your machine via the install command above.
How do I install Investment Scanner Agent Jev in Claude Desktop, Claude Code or Cursor?
Open Investment Scanner Agent Jev on unyly.org, pick your client tab (Claude Desktop, Claude Code, Cursor) and press Install — the config is generated automatically, no JSON editing.
Related MCPs
Opencode Omniroute Plugin
OpenCode plugin for the OmniRoute AI Gateway. Drives dynamic model discovery, /connect auth flow, and multi-instance OmniRoute providers via the official @openc
by GitHub ActionsRoblox Studio
Enables AI coding tools to control Roblox Studio for workspace exploration, instance manipulation, and script management. It provides tools for playtesting, sce
by paralovAntigravity Drawio
MCP server that enables AI assistants to create, parse, render, and validate Draw.io diagrams programmatically.
by S-SUJAN-SFetch
Web content fetching and conversion for efficient LLM usage.
AWS KB Retrieval
Retrieval from AWS Knowledge Base using Bedrock Agent Runtime.
by modelcontextprotocolSpring AI MCP Server
Provides auto-configuration for setting up an MCP server in Spring Boot applications.
llm-analysis-assistant
A very streamlined mcp client that supports calling and monitoring stdio/sse/streamableHttp, and can also view request responses through the /logs page. It also
by xuzexin-hzMCP-Agent
A simple, composable framework to build agents using Model Context Protocol by [LastMile AI](https://www.lastmileai.dev)
by lastmile-aiSpring AI MCP Client
Provides auto-configuration for MCP client functionality in Spring Boot applications.
mcp.natoma.ai
A Hosted MCP Platform to discover, install, manage and deploy MCP servers by [Natoma Labs](https://www.natoma.ai)
Compare Investment Scanner Agent Jev with
Not sure what to pick?
Find your stack in 60 seconds
Author?
Embed badge for your README
Browse similar
All ai MCPs
