ollama-robin 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2 @@
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
@@ -0,0 +1,17 @@
1
+ # Local environment variables (Contains passwords and API keys)
2
+ .env
3
+ .env.local
4
+ .env.*
5
+
6
+ # Python Cache & Build
7
+ __pycache__/
8
+ *.py[cod]
9
+ *$py.class
10
+ .pytest_cache/
11
+ .venv/
12
+ venv/
13
+ env/
14
+
15
+ # OS files
16
+ .DS_Store
17
+ Thumbs.db
@@ -0,0 +1,88 @@
1
+ Metadata-Version: 2.4
2
+ Name: ollama-robin
3
+ Version: 0.1.0
4
+ Summary: Terminal-based AI assistant powered by local Ollama with Exa search and Robinhood tools
5
+ Author-email: Isaiah <isaiahgwood@gmail.com>
6
+ License: MIT
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Operating System :: OS Independent
9
+ Classifier: Programming Language :: Python :: 3
10
+ Requires-Python: >=3.10
11
+ Requires-Dist: exa-py>=2.13.2
12
+ Requires-Dist: ollama>=0.6.2
13
+ Requires-Dist: python-dotenv>=1.0.1
14
+ Requires-Dist: robin-stocks>=3.0.0
15
+ Description-Content-Type: text/markdown
16
+
17
+ # Ollama Robinhood Chat 🤖📈
18
+
19
+ An interactive, terminal-based AI assistant powered by local Ollama (`gemma4:e4b`) with built-in tools for **Exa Web Search** and complete **Robinhood Trading & Portfolio Automation** via the `robin_stocks` SDK.
20
+
21
+ ---
22
+
23
+ ## Key Features
24
+
25
+ - **Local AI Chat**: Chat natively with `gemma4:e4b` running on your local Ollama instance.
26
+ - **Web Search Tool**: Powered by Exa Search API to retrieve real-time news, information, and answers.
27
+ - **Robinhood Portfolio Analytics**: Fetch accounts profile details, portfolio values, cash balances, buying power, and open stock/options positions.
28
+ - **Equities & Options Trading**: Query real-time quotes, chains, historical charts, list watchlists, place market/limit orders, and cancel open orders.
29
+ - **Pre-Trade Simulation Reviews**: Safe order review tools calculate estimated costs, check buying power, and inspect asset tradability flags before committing any trades.
30
+ - **Smart Decimal Formatting**: Automatically sanitizes raw Robinhood API outputs (e.g., converting `$2.7100` to `$2.71`) for clean AI communication.
31
+ - **Interactive Multi-Factor Authentication (MFA)**: Prompts dynamically in the terminal to securely type in SMS or authenticator codes upon boot.
32
+
33
+ ---
34
+
35
+ ## Project Structure
36
+
37
+ ```text
38
+ ollama-robin/
39
+ ├── .env.example # Template env file for credentials
40
+ ├── .gitignore # Prevents committing API keys / secrets
41
+ ├── README.md # Guide & setup documentation
42
+ ├── requirements.txt # Pinned python dependency packages
43
+ ├── chat.py # Main interactive CLI chat loop
44
+ └── tools/
45
+ ├── __init__.py
46
+ ├── formatter.py # Recursive clean-up helper (2.7100 -> 2.71)
47
+ ├── schemas.py # Declarative JSON schemas for Ollama tools
48
+ └── handlers.py # Python logic calling Exa and Robinhood APIs
49
+ ```
50
+
51
+ ---
52
+
53
+ ## Installation & Setup
54
+
55
+ ### 1. Prerequisites
56
+ - [Python 3.10+](https://www.python.org/)
57
+ - [Ollama](https://ollama.com/) running locally with `gemma4:e4b` pulled:
58
+ ```bash
59
+ ollama pull gemma4:e4b
60
+ ```
61
+
62
+ ### 2. Install Python Dependencies
63
+ ```bash
64
+ pip install ollama exa-py robin_stocks python-dotenv
65
+ ```
66
+
67
+ ### 3. Configure Environment Variables
68
+ Create a file named `.env.local` in the project root:
69
+ ```env
70
+ # Exa API Key
71
+ EXA_API_KEY="your-exa-api-key-here"
72
+
73
+ # Robinhood Credentials
74
+ ROBINHOOD_USERNAME="your-login-email@example.com"
75
+ ROBINHOOD_PASSWORD="your-robinhood-password"
76
+ ```
77
+
78
+ ---
79
+
80
+ ## Running the Application
81
+
82
+ To start the interactive chat client:
83
+ ```bash
84
+ python chat.py
85
+ ```
86
+
87
+ - **MFA Warning**: If your Robinhood account has MFA active, the script will pause and prompt you to input the code directly into the terminal window during startup.
88
+ - **Fallback Mode**: If you run without Robinhood credentials, the chat will automatically disable Robinhood tools and fall back to standard chat + Exa web search.
@@ -0,0 +1,72 @@
1
+ # Ollama Robinhood Chat 🤖📈
2
+
3
+ An interactive, terminal-based AI assistant powered by local Ollama (`gemma4:e4b`) with built-in tools for **Exa Web Search** and complete **Robinhood Trading & Portfolio Automation** via the `robin_stocks` SDK.
4
+
5
+ ---
6
+
7
+ ## Key Features
8
+
9
+ - **Local AI Chat**: Chat natively with `gemma4:e4b` running on your local Ollama instance.
10
+ - **Web Search Tool**: Powered by Exa Search API to retrieve real-time news, information, and answers.
11
+ - **Robinhood Portfolio Analytics**: Fetch accounts profile details, portfolio values, cash balances, buying power, and open stock/options positions.
12
+ - **Equities & Options Trading**: Query real-time quotes, chains, historical charts, list watchlists, place market/limit orders, and cancel open orders.
13
+ - **Pre-Trade Simulation Reviews**: Safe order review tools calculate estimated costs, check buying power, and inspect asset tradability flags before committing any trades.
14
+ - **Smart Decimal Formatting**: Automatically sanitizes raw Robinhood API outputs (e.g., converting `$2.7100` to `$2.71`) for clean AI communication.
15
+ - **Interactive Multi-Factor Authentication (MFA)**: Prompts dynamically in the terminal to securely type in SMS or authenticator codes upon boot.
16
+
17
+ ---
18
+
19
+ ## Project Structure
20
+
21
+ ```text
22
+ ollama-robin/
23
+ ├── .env.example # Template env file for credentials
24
+ ├── .gitignore # Prevents committing API keys / secrets
25
+ ├── README.md # Guide & setup documentation
26
+ ├── requirements.txt # Pinned python dependency packages
27
+ ├── chat.py # Main interactive CLI chat loop
28
+ └── tools/
29
+ ├── __init__.py
30
+ ├── formatter.py # Recursive clean-up helper (2.7100 -> 2.71)
31
+ ├── schemas.py # Declarative JSON schemas for Ollama tools
32
+ └── handlers.py # Python logic calling Exa and Robinhood APIs
33
+ ```
34
+
35
+ ---
36
+
37
+ ## Installation & Setup
38
+
39
+ ### 1. Prerequisites
40
+ - [Python 3.10+](https://www.python.org/)
41
+ - [Ollama](https://ollama.com/) running locally with `gemma4:e4b` pulled:
42
+ ```bash
43
+ ollama pull gemma4:e4b
44
+ ```
45
+
46
+ ### 2. Install Python Dependencies
47
+ ```bash
48
+ pip install ollama exa-py robin_stocks python-dotenv
49
+ ```
50
+
51
+ ### 3. Configure Environment Variables
52
+ Create a file named `.env.local` in the project root:
53
+ ```env
54
+ # Exa API Key
55
+ EXA_API_KEY="your-exa-api-key-here"
56
+
57
+ # Robinhood Credentials
58
+ ROBINHOOD_USERNAME="your-login-email@example.com"
59
+ ROBINHOOD_PASSWORD="your-robinhood-password"
60
+ ```
61
+
62
+ ---
63
+
64
+ ## Running the Application
65
+
66
+ To start the interactive chat client:
67
+ ```bash
68
+ python chat.py
69
+ ```
70
+
71
+ - **MFA Warning**: If your Robinhood account has MFA active, the script will pause and prompt you to input the code directly into the terminal window during startup.
72
+ - **Fallback Mode**: If you run without Robinhood credentials, the chat will automatically disable Robinhood tools and fall back to standard chat + Exa web search.
@@ -0,0 +1,28 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "ollama-robin"
7
+ version = "0.1.0"
8
+ description = "Terminal-based AI assistant powered by local Ollama with Exa search and Robinhood tools"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [
13
+ { name = "Isaiah", email = "isaiahgwood@gmail.com" }
14
+ ]
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Operating System :: OS Independent",
19
+ ]
20
+ dependencies = [
21
+ "ollama>=0.6.2",
22
+ "exa-py>=2.13.2",
23
+ "robin_stocks>=3.0.0",
24
+ "python-dotenv>=1.0.1",
25
+ ]
26
+
27
+ [project.scripts]
28
+ ollama-robin = "ollama_robin.chat:main"
@@ -0,0 +1,4 @@
1
+ ollama>=0.6.2
2
+ exa-py>=2.13.2
3
+ robin_stocks>=3.0.0
4
+ python-dotenv>=1.0.1
@@ -0,0 +1 @@
1
+ # Package initialization
@@ -0,0 +1,139 @@
1
+ import os
2
+ import sys
3
+ import ollama
4
+ from exa_py import Exa
5
+ import robin_stocks.robinhood as rh
6
+ from dotenv import load_dotenv
7
+
8
+ # Import our modular tools package
9
+ from ollama_robin.tools.schemas import tools
10
+ from ollama_robin.tools.handlers import handle_tool_call
11
+
12
+ # Load environment variables
13
+ load_dotenv()
14
+ load_dotenv('.env.local')
15
+
16
+ def main():
17
+ model = 'gemma4:e4b'
18
+
19
+ # Verify/get model list
20
+ try:
21
+ response = ollama.list()
22
+ models = [m.model for m in response.models]
23
+ if not models:
24
+ print("No models found. Please pull a model first.")
25
+ sys.exit(1)
26
+ if model not in models and len(models) > 0:
27
+ matching_models = [m for m in models if 'gemma4' in m]
28
+ model = matching_models[0] if matching_models else models[0]
29
+ except Exception as e:
30
+ print(f"Warning: Could not connect to Ollama. Error: {e}")
31
+ sys.exit(1)
32
+
33
+ # Check for Exa API Key
34
+ exa_api_key = os.environ.get("EXA_API_KEY")
35
+ exa_client = None
36
+ if exa_api_key:
37
+ exa_client = Exa(api_key=exa_api_key)
38
+ print("Exa Search Tool enabled.")
39
+ else:
40
+ print("Warning: EXA_API_KEY not found in environment. Exa Search Tool is disabled.")
41
+
42
+ # Check for Robinhood login
43
+ rb_username = os.environ.get("ROBINHOOD_USERNAME")
44
+ rb_password = os.environ.get("ROBINHOOD_PASSWORD")
45
+ rb_enabled = False
46
+
47
+ if rb_username and rb_password:
48
+ try:
49
+ print("Attempting login to Robinhood...")
50
+ login_res = rh.login(username=rb_username, password=rb_password)
51
+ if login_res:
52
+ rb_enabled = True
53
+ print("Successfully logged into Robinhood.")
54
+ else:
55
+ print("Warning: Robinhood login failed.")
56
+ except Exception as e:
57
+ print(f"Warning: Could not login to Robinhood. Error: {e}")
58
+ else:
59
+ print("Warning: ROBINHOOD_USERNAME/ROBINHOOD_PASSWORD not set. Robinhood tools disabled.")
60
+
61
+ # Active tools list based on configuration
62
+ active_tools = []
63
+ if exa_client:
64
+ active_tools.append(tools[0]) # search_web
65
+ if rb_enabled:
66
+ active_tools.extend(tools[1:])
67
+
68
+ print(f"--- Chatting with model: {model} (type 'exit' or 'quit' to stop) ---")
69
+ messages = []
70
+
71
+ while True:
72
+ try:
73
+ user_input = input("\nYou: ").strip()
74
+ if not user_input:
75
+ continue
76
+ if user_input.lower() in ['exit', 'quit']:
77
+ print("Goodbye!")
78
+ break
79
+
80
+ messages.append({'role': 'user', 'content': user_input})
81
+
82
+ # Send message with active tools
83
+ response = ollama.chat(
84
+ model=model,
85
+ messages=messages,
86
+ tools=active_tools if active_tools else None
87
+ )
88
+
89
+ assistant_message = response.get('message', {})
90
+ tool_calls = assistant_message.get('tool_calls', [])
91
+
92
+ thinking = assistant_message.get('content', '')
93
+ if thinking:
94
+ print(f"\n{model} (thinking):\n{thinking}")
95
+
96
+ # If model requested tool execution
97
+ if tool_calls:
98
+ messages.append(assistant_message)
99
+
100
+ for tool_call in tool_calls:
101
+ func_name = tool_call.get('function', {}).get('name')
102
+ args = tool_call.get('function', {}).get('arguments', {})
103
+
104
+ tool_response = handle_tool_call(func_name, args, exa_client, rb_enabled)
105
+
106
+ messages.append({
107
+ 'role': 'tool',
108
+ 'name': func_name,
109
+ 'content': tool_response
110
+ })
111
+
112
+ # Fetch final response after tools execution
113
+ print(f"{model}: ", end="", flush=True)
114
+ response_content = ""
115
+
116
+ # Stream the final response
117
+ stream = ollama.chat(model=model, messages=messages, stream=True)
118
+ for chunk in stream:
119
+ content = chunk['message']['content']
120
+ print(content, end="", flush=True)
121
+ response_content += content
122
+ print()
123
+
124
+ messages.append({'role': 'assistant', 'content': response_content})
125
+
126
+ else:
127
+ content = assistant_message.get('content', '')
128
+ if not thinking:
129
+ print(f"{model}: {content}")
130
+ messages.append(assistant_message)
131
+
132
+ except KeyboardInterrupt:
133
+ print("\nGoodbye!")
134
+ break
135
+ except Exception as e:
136
+ print(f"\nError: {e}")
137
+
138
+ if __name__ == '__main__':
139
+ main()
@@ -0,0 +1 @@
1
+ # Tools package initialization
@@ -0,0 +1,22 @@
1
+ import json
2
+
3
+ def format_numeric_values(data):
4
+ if isinstance(data, dict):
5
+ return {k: format_numeric_values(v) for k, v in data.items()}
6
+ elif isinstance(data, list):
7
+ return [format_numeric_values(v) for v in data]
8
+ elif isinstance(data, str):
9
+ try:
10
+ if '.' in data:
11
+ val = float(data)
12
+ if abs(val) >= 0.01:
13
+ return f"{val:.2f}"
14
+ except ValueError:
15
+ pass
16
+ elif isinstance(data, float):
17
+ if abs(data) >= 0.01:
18
+ return round(data, 2)
19
+ return data
20
+
21
+ def serialize(data):
22
+ return json.dumps(format_numeric_values(data), indent=2)
@@ -0,0 +1,369 @@
1
+ import json
2
+ import robin_stocks.robinhood as rh
3
+ from ollama_robin.tools.formatter import serialize
4
+
5
+ def handle_tool_call(func_name, args, exa_client, rb_enabled):
6
+ tool_response = ""
7
+
8
+ # 1. Search Web
9
+ if func_name == 'search_web' and exa_client:
10
+ query = args.get('query')
11
+ try:
12
+ search_results = exa_client.search(
13
+ query, type="auto", num_results=5, contents={"highlights": True}
14
+ )
15
+ formatted_results = []
16
+ for idx, result in enumerate(search_results.results, 1):
17
+ title = result.title or "Untitled"
18
+ url = result.url
19
+ highlight = "\n".join(result.highlights) if result.highlights else ""
20
+ formatted_results.append(f"[{idx}] {title}\nURL: {url}\nExcerpt: {highlight}\n")
21
+ tool_response = "\n".join(formatted_results) if formatted_results else "No results found."
22
+ except Exception as err:
23
+ tool_response = f"Error performing search: {err}"
24
+
25
+ # 2. Get Accounts
26
+ elif func_name == 'get_accounts' and rb_enabled:
27
+ try:
28
+ tool_response = serialize(rh.profiles.load_account_profile())
29
+ except Exception as err:
30
+ tool_response = f"Error: {err}"
31
+
32
+ # 3. Get Portfolio
33
+ elif func_name == 'get_portfolio' and rb_enabled:
34
+ try:
35
+ portfolio = rh.profiles.load_portfolio_profile()
36
+ basic = rh.profiles.load_basic_profile()
37
+ combined = {
38
+ "equity": portfolio.get("equity"),
39
+ "extended_hours_equity": portfolio.get("extended_hours_equity"),
40
+ "market_value": portfolio.get("market_value"),
41
+ "excess_margin": portfolio.get("excess_margin"),
42
+ "buying_power": portfolio.get("buying_power"),
43
+ "cash": portfolio.get("cash"),
44
+ "cash_available_for_withdrawal": portfolio.get("cash_available_for_withdrawal"),
45
+ "user_first_name": basic.get("first_name")
46
+ }
47
+ tool_response = serialize(combined)
48
+ except Exception as err:
49
+ tool_response = f"Error: {err}"
50
+
51
+ # 4. Get Equity Positions
52
+ elif func_name == 'get_equity_positions' and rb_enabled:
53
+ try:
54
+ positions = rh.get_open_stock_positions()
55
+ for pos in positions:
56
+ try:
57
+ pos['symbol'] = rh.get_symbol_by_url(pos['instrument'])
58
+ except:
59
+ pos['symbol'] = "UNKNOWN"
60
+ tool_response = serialize(positions)
61
+ except Exception as err:
62
+ tool_response = f"Error: {err}"
63
+
64
+ # 5. Get Equity Quotes
65
+ elif func_name == 'get_equity_quotes' and rb_enabled:
66
+ syms = [s.strip().upper() for s in args.get('symbols', '').split(',') if s.strip()]
67
+ try:
68
+ quotes = rh.get_quotes(syms)
69
+ tool_response = serialize(quotes)
70
+ except Exception as err:
71
+ tool_response = f"Error: {err}"
72
+
73
+ # 6. Get Equity Orders
74
+ elif func_name == 'get_equity_orders' and rb_enabled:
75
+ try:
76
+ orders = rh.get_all_stock_orders()[:15]
77
+ tool_response = serialize(orders)
78
+ except Exception as err:
79
+ tool_response = f"Error: {err}"
80
+
81
+ # 7. Get Equity Tradability
82
+ elif func_name == 'get_equity_tradability' and rb_enabled:
83
+ syms = [s.strip().upper() for s in args.get('symbols', '').split(',') if s.strip()]
84
+ try:
85
+ instruments = rh.get_instruments_by_symbols(syms)
86
+ tradability = []
87
+ for inst in instruments:
88
+ if inst:
89
+ tradability.append({
90
+ "symbol": inst.get("symbol"),
91
+ "tradeable": inst.get("tradeable"),
92
+ "fractional_tradability": inst.get("fractional_tradability"),
93
+ "state": inst.get("state")
94
+ })
95
+ tool_response = serialize(tradability)
96
+ except Exception as err:
97
+ tool_response = f"Error: {err}"
98
+
99
+ # 8. Review Equity Order
100
+ elif func_name == 'review_equity_order' and rb_enabled:
101
+ symbol = args.get('symbol').upper()
102
+ qty = float(args.get('quantity'))
103
+ side = args.get('side').lower()
104
+ o_type = args.get('type').lower()
105
+ limit_price = args.get('limit_price')
106
+ try:
107
+ quote = rh.get_quotes(symbol)[0]
108
+ last_price = float(quote.get('last_trade_price', 0)) if quote else 0
109
+ price = limit_price if o_type == 'limit' else last_price
110
+ est_cost = price * qty
111
+ portfolio = rh.profiles.load_portfolio_profile()
112
+ buying_power = float(portfolio.get('buying_power', 0))
113
+ warnings = []
114
+ if side == 'buy' and est_cost > buying_power:
115
+ warnings.append("WARNING: Estimated cost exceeds current buying power.")
116
+ inst = rh.get_instruments_by_symbols(symbol)[0]
117
+ if inst:
118
+ if not inst.get('tradeable'):
119
+ warnings.append("WARNING: Asset is currently not tradeable on Robinhood.")
120
+ if qty % 1 != 0 and not inst.get('fractional_tradability'):
121
+ warnings.append("WARNING: Fractional shares are not supported for this symbol.")
122
+ else:
123
+ warnings.append("WARNING: Ticker symbol could not be found.")
124
+
125
+ review = {
126
+ "symbol": symbol,
127
+ "side": side,
128
+ "quantity": qty,
129
+ "price_used": price,
130
+ "estimated_cost": est_cost,
131
+ "available_buying_power": buying_power,
132
+ "warnings": warnings if warnings else ["None. Order simulation looks good."]
133
+ }
134
+ tool_response = serialize(review)
135
+ except Exception as err:
136
+ tool_response = f"Error reviewing order: {err}"
137
+
138
+ # 9. Place Equity Order
139
+ elif func_name == 'place_equity_order' and rb_enabled:
140
+ symbol = args.get('symbol').upper()
141
+ qty = float(args.get('quantity'))
142
+ side = args.get('side').lower()
143
+ o_type = args.get('type').lower()
144
+ limit_price = args.get('limit_price')
145
+ try:
146
+ if side == 'buy':
147
+ if o_type == 'market':
148
+ res = rh.order_buy_market(symbol, qty)
149
+ else:
150
+ res = rh.order_buy_limit(symbol, qty, limit_price)
151
+ else:
152
+ if o_type == 'market':
153
+ res = rh.order_sell_market(symbol, qty)
154
+ else:
155
+ res = rh.order_sell_limit(symbol, qty, limit_price)
156
+ tool_response = serialize(res)
157
+ except Exception as err:
158
+ tool_response = f"Error placing order: {err}"
159
+
160
+ # 10. Cancel Equity Order
161
+ elif func_name == 'cancel_equity_order' and rb_enabled:
162
+ oid = args.get('order_id')
163
+ try:
164
+ res = rh.cancel_stock_order(oid)
165
+ tool_response = serialize(res)
166
+ except Exception as err:
167
+ tool_response = f"Error: {err}"
168
+
169
+ # 11. Search Symbol
170
+ elif func_name == 'search' and rb_enabled:
171
+ query = args.get('query')
172
+ try:
173
+ res = rh.find_instrument_data(query)
174
+ tool_response = serialize(res[:5])
175
+ except Exception as err:
176
+ tool_response = f"Error: {err}"
177
+
178
+ # 12. Add to Watchlist
179
+ elif func_name == 'add_to_watchlist' and rb_enabled:
180
+ syms = [s.strip().upper() for s in args.get('symbols', '').split(',') if s.strip()]
181
+ w_name = args.get('watchlist_name', 'Default')
182
+ try:
183
+ res = rh.post_symbols_to_watchlist(syms, w_name)
184
+ tool_response = serialize(res)
185
+ except Exception as err:
186
+ tool_response = f"Error: {err}"
187
+
188
+ # 13. Get Watchlists
189
+ elif func_name == 'get_watchlists' and rb_enabled:
190
+ try:
191
+ tool_response = serialize(rh.get_all_watchlists())
192
+ except Exception as err:
193
+ tool_response = f"Error: {err}"
194
+
195
+ # 14. Get Watchlist Items
196
+ elif func_name == 'get_watchlist_items' and rb_enabled:
197
+ w_name = args.get('watchlist_name')
198
+ try:
199
+ tool_response = serialize(rh.get_watchlist_by_name(w_name))
200
+ except Exception as err:
201
+ tool_response = f"Error: {err}"
202
+
203
+ # 15. Unfollow / Remove from Watchlist
204
+ elif func_name == 'unfollow_list' and rb_enabled:
205
+ syms = [s.strip().upper() for s in args.get('symbols', '').split(',') if s.strip()]
206
+ w_name = args.get('watchlist_name', 'Default')
207
+ try:
208
+ res = rh.delete_symbols_from_watchlist(syms, w_name)
209
+ tool_response = serialize(res)
210
+ except Exception as err:
211
+ tool_response = f"Error: {err}"
212
+
213
+ # 16. Get Popular Lists
214
+ elif func_name == 'get_popular_lists' and rb_enabled:
215
+ try:
216
+ tool_response = serialize(rh.get_top_100()[:20])
217
+ except Exception as err:
218
+ tool_response = f"Error: {err}"
219
+
220
+ # 17. Get Equity Historicals
221
+ elif func_name == 'get_equity_historicals' and rb_enabled:
222
+ syms = [s.strip().upper() for s in args.get('symbols', '').split(',') if s.strip()]
223
+ interval = args.get('interval', 'day')
224
+ span = args.get('span', 'year')
225
+ try:
226
+ res = rh.get_stock_historicals(inputSymbols=syms, interval=interval, span=span)
227
+ tool_response = serialize(res)
228
+ except Exception as err:
229
+ tool_response = f"Error: {err}"
230
+
231
+ # 18. Get Indexes
232
+ elif func_name == 'get_indexes' and rb_enabled:
233
+ tool_response = serialize([
234
+ {"index": "S&P 500", "tracked_by_etf": "SPY", "description": "Tracks 500 largest US companies"},
235
+ {"index": "Nasdaq 100", "tracked_by_etf": "QQQ", "description": "Tracks 100 non-financial tech giants"},
236
+ {"index": "Dow Jones 30", "tracked_by_etf": "DIA", "description": "Tracks 30 blue-chip US giants"}
237
+ ])
238
+
239
+ # 19. Get Indexes Quotes
240
+ elif func_name == 'get_indexes_quotes' and rb_enabled:
241
+ try:
242
+ quotes = rh.get_quotes(["SPY", "QQQ", "DIA"])
243
+ formatted = []
244
+ for q in quotes:
245
+ if q:
246
+ formatted.append({
247
+ "index": "S&P 500 (SPY)" if q.get("symbol") == "SPY" else "Nasdaq-100 (QQQ)" if q.get("symbol") == "QQQ" else "Dow-30 (DIA)",
248
+ "price": q.get("last_trade_price"),
249
+ "prior_close": q.get("previous_close")
250
+ })
251
+ tool_response = serialize(formatted)
252
+ except Exception as err:
253
+ tool_response = f"Error: {err}"
254
+
255
+ # 20. Get Option Chains
256
+ elif func_name == 'get_option_chains' and rb_enabled:
257
+ symbol = args.get('symbol').upper()
258
+ try:
259
+ chains = rh.get_chains(symbol)
260
+ tool_response = serialize(chains)
261
+ except Exception as err:
262
+ tool_response = f"Error: {err}"
263
+
264
+ # 21. Get Option Instruments
265
+ elif func_name == 'get_option_instruments' and rb_enabled:
266
+ symbol = args.get('symbol').upper()
267
+ exp = args.get('expiration_date')
268
+ strike = args.get('strike_price')
269
+ o_type = args.get('option_type')
270
+ try:
271
+ res = rh.find_options_by_expiration(inputSymbols=symbol, expirationDate=exp, optionType=o_type)
272
+ if strike:
273
+ res = [o for o in res if float(o.get('strike_price', 0)) == strike]
274
+ tool_response = serialize(res[:10])
275
+ except Exception as err:
276
+ tool_response = f"Error: {err}"
277
+
278
+ # 22. Get Option Quotes
279
+ elif func_name == 'get_option_quotes' and rb_enabled:
280
+ symbol = args.get('symbol').upper()
281
+ exp = args.get('expiration_date')
282
+ strike = float(args.get('strike_price'))
283
+ o_type = args.get('option_type').lower()
284
+ try:
285
+ res = rh.get_option_market_data(symbol, exp, str(strike), o_type)
286
+ tool_response = serialize(res)
287
+ except Exception as err:
288
+ tool_response = f"Error: {err}"
289
+
290
+ # 23. Get Option Positions
291
+ elif func_name == 'get_option_positions' and rb_enabled:
292
+ try:
293
+ tool_response = serialize(rh.get_open_option_positions())
294
+ except Exception as err:
295
+ tool_response = f"Error: {err}"
296
+
297
+ # 24. Get Option Orders
298
+ elif func_name == 'get_option_orders' and rb_enabled:
299
+ try:
300
+ tool_response = serialize(rh.get_all_option_orders()[:10])
301
+ except Exception as err:
302
+ tool_response = f"Error: {err}"
303
+
304
+ # 25. Review Option Order
305
+ elif func_name == 'review_option_order' and rb_enabled:
306
+ symbol = args.get('symbol').upper()
307
+ exp = args.get('expiration_date')
308
+ strike = float(args.get('strike_price'))
309
+ o_type = args.get('option_type').lower()
310
+ qty = float(args.get('quantity'))
311
+ price = float(args.get('price'))
312
+ side = args.get('side').lower()
313
+ try:
314
+ market_data = rh.get_option_market_data(symbol, exp, str(strike), o_type)[0][0]
315
+ ask = float(market_data.get('ask_price', 0))
316
+ bid = float(market_data.get('bid_price', 0))
317
+ est_cost = price * qty * 100
318
+ portfolio = rh.profiles.load_portfolio_profile()
319
+ buying_power = float(portfolio.get('buying_power', 0))
320
+ warnings = []
321
+ if side == 'buy' and est_cost > buying_power:
322
+ warnings.append("WARNING: Estimated debit cost exceeds current buying power.")
323
+
324
+ review = {
325
+ "underlying": symbol,
326
+ "option": f"{strike}{o_type[0]} exp {exp}",
327
+ "side": side,
328
+ "quantity": qty,
329
+ "limit_price": price,
330
+ "ask_price": ask,
331
+ "bid_price": bid,
332
+ "estimated_cost": est_cost,
333
+ "available_buying_power": buying_power,
334
+ "warnings": warnings if warnings else ["None. Simulation looks good."]
335
+ }
336
+ tool_response = serialize(review)
337
+ except Exception as err:
338
+ tool_response = f"Error: {err}"
339
+
340
+ # 26. Place Option Order
341
+ elif func_name == 'place_option_order' and rb_enabled:
342
+ symbol = args.get('symbol').upper()
343
+ exp = args.get('expiration_date')
344
+ strike = float(args.get('strike_price'))
345
+ o_type = args.get('option_type').lower()
346
+ qty = int(args.get('quantity'))
347
+ price = float(args.get('price'))
348
+ side = args.get('side').lower()
349
+ try:
350
+ effect = "open" if side == 'buy' else "close"
351
+ cd = "debit" if side == 'buy' else "credit"
352
+ res = rh.order_buy_option_limit(
353
+ positionEffect=effect, creditOrDebit=cd, price=price, symbol=symbol,
354
+ quantity=qty, expirationDate=exp, strike=strike, optionType=o_type
355
+ )
356
+ tool_response = serialize(res)
357
+ except Exception as err:
358
+ tool_response = f"Error: {err}"
359
+
360
+ # 27. Cancel Option Order
361
+ elif func_name == 'cancel_option_order' and rb_enabled:
362
+ oid = args.get('order_id')
363
+ try:
364
+ res = rh.cancel_option_order(oid)
365
+ tool_response = serialize(res)
366
+ except Exception as err:
367
+ tool_response = f"Error: {err}"
368
+
369
+ return tool_response
@@ -0,0 +1,367 @@
1
+ # Declarative schemas for Exa and Robinhood tools
2
+
3
+ tools = [
4
+ # Exa search tool
5
+ {
6
+ 'type': 'function',
7
+ 'function': {
8
+ 'name': 'search_web',
9
+ 'description': 'Search the web using Exa to get up-to-date information, news, or answers.',
10
+ 'parameters': {
11
+ 'type': 'object',
12
+ 'properties': {
13
+ 'query': {
14
+ 'type': 'string',
15
+ 'description': 'The search query or question to ask.',
16
+ },
17
+ },
18
+ 'required': ['query'],
19
+ },
20
+ },
21
+ },
22
+ # Portfolio & Accounts
23
+ {
24
+ 'type': 'function',
25
+ 'function': {
26
+ 'name': 'get_accounts',
27
+ 'description': 'View all your Robinhood accounts profile information.',
28
+ 'parameters': {'type': 'object', 'properties': {}}
29
+ }
30
+ },
31
+ {
32
+ 'type': 'function',
33
+ 'function': {
34
+ 'name': 'get_portfolio',
35
+ 'description': 'Get a snapshot of your portfolio including total value, buying power, and basic details.',
36
+ 'parameters': {'type': 'object', 'properties': {}}
37
+ }
38
+ },
39
+ {
40
+ 'type': 'function',
41
+ 'function': {
42
+ 'name': 'get_equity_positions',
43
+ 'description': 'View open equity (stock) positions with quantity and cost basis.',
44
+ 'parameters': {'type': 'object', 'properties': {}}
45
+ }
46
+ },
47
+ # Equities
48
+ {
49
+ 'type': 'function',
50
+ 'function': {
51
+ 'name': 'get_equity_quotes',
52
+ 'description': 'Get real-time equity quotes and prior close for up to 20 symbols.',
53
+ 'parameters': {
54
+ 'type': 'object',
55
+ 'properties': {
56
+ 'symbols': {
57
+ 'type': 'string',
58
+ 'description': 'Comma-separated tickers, e.g. "AAPL,MSFT"'
59
+ }
60
+ },
61
+ 'required': ['symbols']
62
+ }
63
+ }
64
+ },
65
+ {
66
+ 'type': 'function',
67
+ 'function': {
68
+ 'name': 'get_equity_orders',
69
+ 'description': 'Get equity order status history.',
70
+ 'parameters': {'type': 'object', 'properties': {}}
71
+ }
72
+ },
73
+ {
74
+ 'type': 'function',
75
+ 'function': {
76
+ 'name': 'get_equity_tradability',
77
+ 'description': 'Check if a list of symbols can be traded and if they can be traded fractionally.',
78
+ 'parameters': {
79
+ 'type': 'object',
80
+ 'properties': {
81
+ 'symbols': {
82
+ 'type': 'string',
83
+ 'description': 'Comma-separated tickers, e.g. "AAPL,TSLA"'
84
+ }
85
+ },
86
+ 'required': ['symbols']
87
+ }
88
+ }
89
+ },
90
+ {
91
+ 'type': 'function',
92
+ 'function': {
93
+ 'name': 'review_equity_order',
94
+ 'description': 'Simulate an equity order and get pre-trade warnings, estimated cost, and checks.',
95
+ 'parameters': {
96
+ 'type': 'object',
97
+ 'properties': {
98
+ 'symbol': {'type': 'string', 'description': 'The stock ticker, e.g. "AAPL"'},
99
+ 'quantity': {'type': 'number', 'description': 'Number of shares'},
100
+ 'side': {'type': 'string', 'enum': ['buy', 'sell'], 'description': 'Buy or sell'},
101
+ 'type': {'type': 'string', 'enum': ['market', 'limit'], 'description': 'Order type'},
102
+ 'limit_price': {'type': 'number', 'description': 'Limit price (required if type is limit)'}
103
+ },
104
+ 'required': ['symbol', 'quantity', 'side', 'type']
105
+ }
106
+ }
107
+ },
108
+ {
109
+ 'type': 'function',
110
+ 'function': {
111
+ 'name': 'place_equity_order',
112
+ 'description': 'Place an actual equity order on Robinhood.',
113
+ 'parameters': {
114
+ 'type': 'object',
115
+ 'properties': {
116
+ 'symbol': {'type': 'string', 'description': 'The stock ticker, e.g. "AAPL"'},
117
+ 'quantity': {'type': 'number', 'description': 'Number of shares'},
118
+ 'side': {'type': 'string', 'enum': ['buy', 'sell'], 'description': 'Buy or sell'},
119
+ 'type': {'type': 'string', 'enum': ['market', 'limit'], 'description': 'Order type'},
120
+ 'limit_price': {'type': 'number', 'description': 'Limit price (required if type is limit)'}
121
+ },
122
+ 'required': ['symbol', 'quantity', 'side', 'type']
123
+ }
124
+ }
125
+ },
126
+ {
127
+ 'type': 'function',
128
+ 'function': {
129
+ 'name': 'cancel_equity_order',
130
+ 'description': 'Cancel an open equity order by ID.',
131
+ 'parameters': {
132
+ 'type': 'object',
133
+ 'properties': {
134
+ 'order_id': {'type': 'string', 'description': 'The unique order ID'}
135
+ },
136
+ 'required': ['order_id']
137
+ }
138
+ }
139
+ },
140
+ {
141
+ 'type': 'function',
142
+ 'function': {
143
+ 'name': 'search',
144
+ 'description': 'Find a company name or partial name to get its ticker symbol.',
145
+ 'parameters': {
146
+ 'type': 'object',
147
+ 'properties': {
148
+ 'query': {'type': 'string', 'description': 'Company name or search phrase'}
149
+ },
150
+ 'required': ['query']
151
+ }
152
+ }
153
+ },
154
+ # Watchlist
155
+ {
156
+ 'type': 'function',
157
+ 'function': {
158
+ 'name': 'add_to_watchlist',
159
+ 'description': 'Add stock symbols to a watchlist.',
160
+ 'parameters': {
161
+ 'type': 'object',
162
+ 'properties': {
163
+ 'symbols': {'type': 'string', 'description': 'Comma-separated tickers to add'},
164
+ 'watchlist_name': {'type': 'string', 'description': 'Watchlist name, default is "Default"'}
165
+ },
166
+ 'required': ['symbols']
167
+ }
168
+ }
169
+ },
170
+ {
171
+ 'type': 'function',
172
+ 'function': {
173
+ 'name': 'get_watchlists',
174
+ 'description': 'List all watchlists on your account.',
175
+ 'parameters': {'type': 'object', 'properties': {}}
176
+ }
177
+ },
178
+ {
179
+ 'type': 'function',
180
+ 'function': {
181
+ 'name': 'get_watchlist_items',
182
+ 'description': 'List symbols inside a specific watchlist.',
183
+ 'parameters': {
184
+ 'type': 'object',
185
+ 'properties': {
186
+ 'watchlist_name': {'type': 'string', 'description': 'Name of the watchlist'}
187
+ },
188
+ 'required': ['watchlist_name']
189
+ }
190
+ }
191
+ },
192
+ {
193
+ 'type': 'function',
194
+ 'function': {
195
+ 'name': 'unfollow_list',
196
+ 'description': 'Remove symbols from a watchlist.',
197
+ 'parameters': {
198
+ 'type': 'object',
199
+ 'properties': {
200
+ 'symbols': {'type': 'string', 'description': 'Comma-separated tickers to remove'},
201
+ 'watchlist_name': {'type': 'string', 'description': 'Watchlist name, default is "Default"'}
202
+ },
203
+ 'required': ['symbols']
204
+ }
205
+ }
206
+ },
207
+ {
208
+ 'type': 'function',
209
+ 'function': {
210
+ 'name': 'get_popular_lists',
211
+ 'description': 'Discover popular Robinhood lists (100 most popular stocks).',
212
+ 'parameters': {'type': 'object', 'properties': {}}
213
+ }
214
+ },
215
+ # Market Data
216
+ {
217
+ 'type': 'function',
218
+ 'function': {
219
+ 'name': 'get_equity_historicals',
220
+ 'description': 'Get OHLCV price bars across a time range.',
221
+ 'parameters': {
222
+ 'type': 'object',
223
+ 'properties': {
224
+ 'symbols': {'type': 'string', 'description': 'Comma-separated tickers, e.g. "AAPL,TSLA"'},
225
+ 'interval': {'type': 'string', 'enum': ['5minute', '10minute', 'hour', 'day', 'week'], 'description': 'Interval of bars'},
226
+ 'span': {'type': 'string', 'enum': ['day', 'week', 'month', '3month', 'year', '5year'], 'description': 'Time span'}
227
+ },
228
+ 'required': ['symbols']
229
+ }
230
+ }
231
+ },
232
+ {
233
+ 'type': 'function',
234
+ 'function': {
235
+ 'name': 'get_indexes',
236
+ 'description': 'Look up market index symbols and descriptors (e.g. S&P 500, Nasdaq, Dow).',
237
+ 'parameters': {'type': 'object', 'properties': {}}
238
+ }
239
+ },
240
+ {
241
+ 'type': 'function',
242
+ 'function': {
243
+ 'name': 'get_indexes_quotes',
244
+ 'description': 'Get real-time values of major indexes (via ETFs SPY, QQQ, DIA).',
245
+ 'parameters': {'type': 'object', 'properties': {}}
246
+ }
247
+ },
248
+ # Options
249
+ {
250
+ 'type': 'function',
251
+ 'function': {
252
+ 'name': 'get_option_chains',
253
+ 'description': 'Load option chain info including available expiration dates.',
254
+ 'parameters': {
255
+ 'type': 'object',
256
+ 'properties': {
257
+ 'symbol': {'type': 'string', 'description': 'Underlying stock ticker'}
258
+ },
259
+ 'required': ['symbol']
260
+ }
261
+ }
262
+ },
263
+ {
264
+ 'type': 'function',
265
+ 'function': {
266
+ 'name': 'get_option_instruments',
267
+ 'description': 'Load option contracts filtered by expiration and strike.',
268
+ 'parameters': {
269
+ 'type': 'object',
270
+ 'properties': {
271
+ 'symbol': {'type': 'string', 'description': 'Underlying stock ticker'},
272
+ 'expiration_date': {'type': 'string', 'description': 'Expiration date (YYYY-MM-DD)'},
273
+ 'strike_price': {'type': 'number', 'description': 'Strike price'},
274
+ 'option_type': {'type': 'string', 'enum': ['call', 'put'], 'description': 'Call or put'}
275
+ },
276
+ 'required': ['symbol', 'expiration_date']
277
+ }
278
+ }
279
+ },
280
+ {
281
+ 'type': 'function',
282
+ 'function': {
283
+ 'name': 'get_option_quotes',
284
+ 'description': 'Get real-time quotes for option contracts.',
285
+ 'parameters': {
286
+ 'type': 'object',
287
+ 'properties': {
288
+ 'symbol': {'type': 'string', 'description': 'Underlying stock ticker'},
289
+ 'expiration_date': {'type': 'string', 'description': 'Expiration date (YYYY-MM-DD)'},
290
+ 'strike_price': {'type': 'number', 'description': 'Strike price'},
291
+ 'option_type': {'type': 'string', 'enum': ['call', 'put'], 'description': 'Call or put'}
292
+ },
293
+ 'required': ['symbol', 'expiration_date', 'strike_price', 'option_type']
294
+ }
295
+ }
296
+ },
297
+ {
298
+ 'type': 'function',
299
+ 'function': {
300
+ 'name': 'get_option_positions',
301
+ 'description': 'View open or closed options positions.',
302
+ 'parameters': {'type': 'object', 'properties': {}}
303
+ }
304
+ },
305
+ {
306
+ 'type': 'function',
307
+ 'function': {
308
+ 'name': 'get_option_orders',
309
+ 'description': 'Get options order history.',
310
+ 'parameters': {'type': 'object', 'properties': {}}
311
+ }
312
+ },
313
+ {
314
+ 'type': 'function',
315
+ 'function': {
316
+ 'name': 'review_option_order',
317
+ 'description': 'Simulate an options order and get pre-trade warnings, bid/ask spreads, and buying power requirements.',
318
+ 'parameters': {
319
+ 'type': 'object',
320
+ 'properties': {
321
+ 'symbol': {'type': 'string', 'description': 'Underlying ticker'},
322
+ 'expiration_date': {'type': 'string', 'description': 'Expiration date (YYYY-MM-DD)'},
323
+ 'strike_price': {'type': 'number', 'description': 'Strike price'},
324
+ 'option_type': {'type': 'string', 'enum': ['call', 'put']},
325
+ 'quantity': {'type': 'number', 'description': 'Number of contracts'},
326
+ 'price': {'type': 'number', 'description': 'Limit price per contract'},
327
+ 'side': {'type': 'string', 'enum': ['buy', 'sell']}
328
+ },
329
+ 'required': ['symbol', 'expiration_date', 'strike_price', 'option_type', 'quantity', 'price', 'side']
330
+ }
331
+ }
332
+ },
333
+ {
334
+ 'type': 'function',
335
+ 'function': {
336
+ 'name': 'place_option_order',
337
+ 'description': 'Place a real options limit order on Robinhood.',
338
+ 'parameters': {
339
+ 'type': 'object',
340
+ 'properties': {
341
+ 'symbol': {'type': 'string', 'description': 'Underlying ticker'},
342
+ 'expiration_date': {'type': 'string', 'description': 'Expiration date (YYYY-MM-DD)'},
343
+ 'strike_price': {'type': 'number', 'description': 'Strike price'},
344
+ 'option_type': {'type': 'string', 'enum': ['call', 'put']},
345
+ 'quantity': {'type': 'number', 'description': 'Number of contracts'},
346
+ 'price': {'type': 'number', 'description': 'Limit price per contract'},
347
+ 'side': {'type': 'string', 'enum': ['buy', 'sell']}
348
+ },
349
+ 'required': ['symbol', 'expiration_date', 'strike_price', 'option_type', 'quantity', 'price', 'side']
350
+ }
351
+ }
352
+ },
353
+ {
354
+ 'type': 'function',
355
+ 'function': {
356
+ 'name': 'cancel_option_order',
357
+ 'description': 'Cancel an open options order by ID.',
358
+ 'parameters': {
359
+ 'type': 'object',
360
+ 'properties': {
361
+ 'order_id': {'type': 'string', 'description': 'The option order ID'}
362
+ },
363
+ 'required': ['order_id']
364
+ }
365
+ }
366
+ }
367
+ ]