Metadata-Version: 2.4
Name: junctionary-pulse-langchain
Version: 0.1.0
Summary: LangChain toolkit for Junctionary Pulse — finished stock-market briefings for AI agents
Author: Junctionary Systems LLC
Project-URL: Homepage, https://www.junctionarypulse.com
Project-URL: Documentation, https://www.junctionarypulse.com/agents
Keywords: langchain,stock-market,briefings,ai-agents,finance
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Classifier: Topic :: Office/Business :: Financial
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Requires-Dist: langchain-core>=0.3
Requires-Dist: httpx>=0.27
Requires-Dist: pydantic>=2

# Junctionary Pulse — LangChain Toolkit

Give your LangChain agents finished stock-market briefings: one tool call per
ticker, every weekday morning. No raw-news plumbing, no 50k-token web-research
loops — just structured briefings with sources.

- **Product:** [junctionarypulse.com](https://www.junctionarypulse.com) — $5/mo agent plan, any 5 tickers
- **Auth:** `X-API-Key` header (get a key at [/agents](https://www.junctionarypulse.com/agents))
- **Rate limit:** 120 requests/min

## Installation

```bash
pip install langchain-core httpx
```

Copy `junctionary_toolkit.py` into your project (or add this directory to
`PYTHONPATH`).

## Usage

```python
import os
from langchain.agents import create_agent
from junctionary_toolkit import JunctionaryPulseToolkit

toolkit = JunctionaryPulseToolkit(
    api_key=os.environ["JUNCTIONARY_API_KEY"],
)

# Works with any LangChain agent — tools are just BaseTools
agent = create_agent(
    model="claude-haiku-4-5",
    tools=toolkit.get_tools(),
    system_prompt="You are a market research assistant. Use Junctionary Pulse "
                  "tools for stock news; always cite sources from the briefing.",
)

result = agent.invoke({
    "messages": [{"role": "user",
                  "content": "What's the latest on Nvidia, and is sentiment improving?"}]
})
print(result["messages"][-1].content)
```

With a lower-level chain, call tools directly:

```python
tools = {t.name: t for t in toolkit.get_tools()}

briefing = tools["get_briefing"].invoke({"pulse_id": "nvidia"})
history  = tools["get_briefing_history"].invoke({"pulse_id": "nvidia", "days": 14})

# async works too
briefing = await tools["get_briefing"].ainvoke({"pulse_id": "nvidia"})
```

## Tools

| Tool | Arguments | When the agent uses it |
|---|---|---|
| `list_pulses` | — | Discover which tickers have briefings; call first when unsure of a `pulse_id` |
| `get_watchlist_digest` | — | **Start here every morning.** One call returns today's headline, TL;DR, price, and story count for EVERY subscribed ticker — 1 API call instead of N |
| `get_briefing` | `pulse_id` | "What's happening with X today?" — today's full briefing (headline, TL;DR, stories, sources) |
| `get_briefing_for_date` | `pulse_id`, `date` (YYYY-MM-DD) | Past news, catching up on missed days, backtesting, training data (90-day backfill) |
| `get_briefing_history` | `pulse_id`, `days` (1–90) | Multi-day overview: headline + story count + sentiment label per day — scan this before pulling individual days |
| `get_sentiment_trends` | `pulse_id`, `days` (1–90) | "Is sentiment turning negative?" — numeric sentiment scores and volume trends |
| `compare_tickers` | `ticker_a`, `ticker_b`, `days` (1–90) | "How does NVDA compare to AMD?" — side-by-side sentiment scores, trend direction, and a plain-English verdict |
| `get_earnings_date` | `ticker` | "When does NVDA report?" — next earnings date for a ticker |
| `resolve_ticker` | `query` | Company name → ticker symbol ("Apple" → AAPL). Call first when you have a name but not a symbol |
| `get_alerts` | `tickers?`, `since?` | Intraday price-move alerts (not news — use `get_briefing` for news) |

### Reading the room: which tool when

- **"What's happening today?" / morning catch-up** → `get_watchlist_digest` (1 call, everything)
- **"Latest news on TSLA"** → `get_briefing`
- **"Catch me up on Apple this month"** → `get_briefing_history`, then `get_briefing_for_date` for the interesting days
- **"Is NVDA sentiment improving?"** → `get_sentiment_trends`
- **"NVDA vs AMD — which looks better?"** → `compare_tickers`
- **"When does Apple report earnings?"** → `get_earnings_date`
- **"What's the ticker for ServiceNow?"** → `resolve_ticker`
- **"What happened with MSFT last Tuesday?"** → `get_briefing_for_date`
- **"Did anything move today?"** → `get_alerts`
- **"What do you cover?"** → `list_pulses`

### Token efficiency

`get_watchlist_digest` is the cheapest way to orient an agent: one HTTP
call returns a compact row per ticker (headline, TL;DR, previous close,
story count) instead of one full `get_briefing` call per ticker. For a
5-ticker subscription, that's roughly **1/10th the tokens** of pulling
five full briefings. Scan the digest first, then `get_briefing` only for
the tickers that matter.

## Error handling

Tools never raise into the agent loop. Failures come back as plain-text
messages the agent can act on:

| Situation | Tool returns |
|---|---|
| Bad API key (401) | `Error: Invalid API key. Get a key at …` |
| Ticker not in subscription (403) | `Error: Not entitled to this ticker …` |
| Unknown ticker/date (404) | `Error: Not found. …` |
| Rate limited (429) | `Error: Rate limited (120/min). Retry after N seconds.` |
| Network failure | `Error: Could not reach the Junctionary Pulse API: …` |

## Configuration

```python
JunctionaryPulseToolkit(
    api_key="jp_...",                              # or JUNCTIONARY_API_KEY env var
    api_base="https://api.junctionarypulse.com",   # override for staging/tests
)
```

## Tests

```bash
python3 -m pytest test_toolkit.py -v
```

All HTTP is mocked — no live API calls, no API key needed.
