feat: dynamic stock search via Sina suggest API

- Add searchStock() using Sina's suggest API (type=11,31,41)
- Supports A-share, HK, and US markets dynamically
- No longer limited to hardcoded knownStocks map
- resolveStockCodeDynamic: tries static map first, then API fallback
- extractStockKeyword: smart keyword extraction from natural language
- New LLM tool 'search_stock': agent can proactively search stocks
- Top 3 search results auto-enriched with real-time quotes
- System prompts updated for both zh/en
This commit is contained in:
shinchan-zhai
2026-03-23 02:54:20 +08:00
parent 4cba7edd19
commit 5676ba2e5c
3 changed files with 248 additions and 2 deletions

View File

@@ -13,6 +13,23 @@ import (
// agentTools defines the tools available to the LLM for autonomous action.
func agentTools() []mcp.Tool {
return []mcp.Tool{
{
Type: "function",
Function: mcp.FunctionDef{
Name: "search_stock",
Description: "Search for a stock by name, ticker symbol, or keyword. Searches across A-share (沪深), Hong Kong, and US markets. Returns a list of matching stocks with their codes. Use this when the user asks about a stock not in your known list, or when you need to find the exact code for a stock.",
Parameters: map[string]any{
"type": "object",
"properties": map[string]any{
"keyword": map[string]any{
"type": "string",
"description": "Search keyword: stock name (e.g. '宁德时代', '腾讯'), ticker (e.g. 'TSLA', 'AAPL'), or stock code (e.g. '300750')",
},
},
"required": []string{"keyword"},
},
},
},
{
Type: "function",
Function: mcp.FunctionDef{
@@ -82,6 +99,8 @@ func agentTools() []mcp.Tool {
// handleToolCall processes a single tool call from the LLM and returns the result.
func (a *Agent) handleToolCall(ctx context.Context, userID int64, lang string, tc mcp.ToolCall) string {
switch tc.Function.Name {
case "search_stock":
return a.toolSearchStock(tc.Function.Arguments)
case "execute_trade":
return a.toolExecuteTrade(ctx, userID, lang, tc.Function.Arguments)
case "get_positions":
@@ -95,6 +114,60 @@ func (a *Agent) handleToolCall(ctx context.Context, userID int64, lang string, t
}
}
func (a *Agent) toolSearchStock(argsJSON string) string {
var args struct {
Keyword string `json:"keyword"`
}
if err := json.Unmarshal([]byte(argsJSON), &args); err != nil {
return fmt.Sprintf(`{"error": "invalid arguments: %s"}`, err)
}
if args.Keyword == "" {
return `{"error": "keyword is required"}`
}
results, err := searchStock(args.Keyword)
if err != nil {
return fmt.Sprintf(`{"error": "search failed: %s"}`, err)
}
if len(results) == 0 {
return fmt.Sprintf(`{"results": [], "message": "no stocks found for '%s'"}`, args.Keyword)
}
// Limit to top 10 results
if len(results) > 10 {
results = results[:10]
}
// Also fetch real-time quotes for the top results (up to 3)
type enrichedResult struct {
Name string `json:"name"`
Code string `json:"code"`
Market string `json:"market"`
Quote *StockQuote `json:"quote,omitempty"`
}
var enriched []enrichedResult
for i, r := range results {
er := enrichedResult{Name: r.Name, Code: r.Code, Market: r.Market}
if i < 3 {
q, qErr := fetchStockQuote(r.Code)
if qErr == nil && q.Price > 0 {
er.Quote = q
}
}
enriched = append(enriched, er)
}
result, _ := json.Marshal(map[string]any{
"keyword": args.Keyword,
"count": len(enriched),
"results": enriched,
})
return string(result)
}
func (a *Agent) toolExecuteTrade(_ context.Context, userID int64, lang, argsJSON string) string {
var args struct {
Action string `json:"action"`