@indexnetwork/protocol 0.1.0
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.
- package/dist/agents/chat.agent.d.ts +218 -0
- package/dist/agents/chat.agent.d.ts.map +1 -0
- package/dist/agents/chat.agent.js +884 -0
- package/dist/agents/chat.agent.js.map +1 -0
- package/dist/agents/chat.prompt.d.ts +18 -0
- package/dist/agents/chat.prompt.d.ts.map +1 -0
- package/dist/agents/chat.prompt.js +372 -0
- package/dist/agents/chat.prompt.js.map +1 -0
- package/dist/agents/chat.prompt.modules.d.ts +61 -0
- package/dist/agents/chat.prompt.modules.d.ts.map +1 -0
- package/dist/agents/chat.prompt.modules.js +366 -0
- package/dist/agents/chat.prompt.modules.js.map +1 -0
- package/dist/agents/chat.title.generator.d.ts +20 -0
- package/dist/agents/chat.title.generator.d.ts.map +1 -0
- package/dist/agents/chat.title.generator.js +66 -0
- package/dist/agents/chat.title.generator.js.map +1 -0
- package/dist/agents/home.categorizer.d.ts +28 -0
- package/dist/agents/home.categorizer.d.ts.map +1 -0
- package/dist/agents/home.categorizer.js +170 -0
- package/dist/agents/home.categorizer.js.map +1 -0
- package/dist/agents/hyde.generator.d.ts +27 -0
- package/dist/agents/hyde.generator.d.ts.map +1 -0
- package/dist/agents/hyde.generator.js +75 -0
- package/dist/agents/hyde.generator.js.map +1 -0
- package/dist/agents/hyde.strategies.d.ts +17 -0
- package/dist/agents/hyde.strategies.d.ts.map +1 -0
- package/dist/agents/hyde.strategies.js +29 -0
- package/dist/agents/hyde.strategies.js.map +1 -0
- package/dist/agents/intent.clarifier.d.ts +29 -0
- package/dist/agents/intent.clarifier.d.ts.map +1 -0
- package/dist/agents/intent.clarifier.js +186 -0
- package/dist/agents/intent.clarifier.js.map +1 -0
- package/dist/agents/intent.indexer.d.ts +77 -0
- package/dist/agents/intent.indexer.d.ts.map +1 -0
- package/dist/agents/intent.indexer.js +164 -0
- package/dist/agents/intent.indexer.js.map +1 -0
- package/dist/agents/intent.inferrer.d.ts +95 -0
- package/dist/agents/intent.inferrer.d.ts.map +1 -0
- package/dist/agents/intent.inferrer.js +238 -0
- package/dist/agents/intent.inferrer.js.map +1 -0
- package/dist/agents/intent.reconciler.d.ts +106 -0
- package/dist/agents/intent.reconciler.d.ts.map +1 -0
- package/dist/agents/intent.reconciler.js +184 -0
- package/dist/agents/intent.reconciler.js.map +1 -0
- package/dist/agents/intent.verifier.d.ts +97 -0
- package/dist/agents/intent.verifier.d.ts.map +1 -0
- package/dist/agents/intent.verifier.js +234 -0
- package/dist/agents/intent.verifier.js.map +1 -0
- package/dist/agents/invite.generator.d.ts +47 -0
- package/dist/agents/invite.generator.d.ts.map +1 -0
- package/dist/agents/invite.generator.js +56 -0
- package/dist/agents/invite.generator.js.map +1 -0
- package/dist/agents/lens.inferrer.d.ts +37 -0
- package/dist/agents/lens.inferrer.d.ts.map +1 -0
- package/dist/agents/lens.inferrer.js +98 -0
- package/dist/agents/lens.inferrer.js.map +1 -0
- package/dist/agents/model.config.d.ts +120 -0
- package/dist/agents/model.config.d.ts.map +1 -0
- package/dist/agents/model.config.js +76 -0
- package/dist/agents/model.config.js.map +1 -0
- package/dist/agents/negotiation.insights.generator.d.ts +32 -0
- package/dist/agents/negotiation.insights.generator.d.ts.map +1 -0
- package/dist/agents/negotiation.insights.generator.js +105 -0
- package/dist/agents/negotiation.insights.generator.js.map +1 -0
- package/dist/agents/negotiation.proposer.d.ts +26 -0
- package/dist/agents/negotiation.proposer.d.ts.map +1 -0
- package/dist/agents/negotiation.proposer.js +67 -0
- package/dist/agents/negotiation.proposer.js.map +1 -0
- package/dist/agents/negotiation.responder.d.ts +26 -0
- package/dist/agents/negotiation.responder.d.ts.map +1 -0
- package/dist/agents/negotiation.responder.js +71 -0
- package/dist/agents/negotiation.responder.js.map +1 -0
- package/dist/agents/opportunity.evaluator.d.ts +253 -0
- package/dist/agents/opportunity.evaluator.d.ts.map +1 -0
- package/dist/agents/opportunity.evaluator.js +413 -0
- package/dist/agents/opportunity.evaluator.js.map +1 -0
- package/dist/agents/opportunity.presenter.d.ts +115 -0
- package/dist/agents/opportunity.presenter.d.ts.map +1 -0
- package/dist/agents/opportunity.presenter.js +524 -0
- package/dist/agents/opportunity.presenter.js.map +1 -0
- package/dist/agents/profile.generator.d.ts +67 -0
- package/dist/agents/profile.generator.d.ts.map +1 -0
- package/dist/agents/profile.generator.js +97 -0
- package/dist/agents/profile.generator.js.map +1 -0
- package/dist/agents/profile.hyde.generator.d.ts +43 -0
- package/dist/agents/profile.hyde.generator.d.ts.map +1 -0
- package/dist/agents/profile.hyde.generator.js +113 -0
- package/dist/agents/profile.hyde.generator.js.map +1 -0
- package/dist/agents/suggestion.generator.d.ts +24 -0
- package/dist/agents/suggestion.generator.d.ts.map +1 -0
- package/dist/agents/suggestion.generator.js +96 -0
- package/dist/agents/suggestion.generator.js.map +1 -0
- package/dist/graphs/chat.graph.d.ts +312 -0
- package/dist/graphs/chat.graph.d.ts.map +1 -0
- package/dist/graphs/chat.graph.js +267 -0
- package/dist/graphs/chat.graph.js.map +1 -0
- package/dist/graphs/home.graph.d.ts +180 -0
- package/dist/graphs/home.graph.d.ts.map +1 -0
- package/dist/graphs/home.graph.js +598 -0
- package/dist/graphs/home.graph.js.map +1 -0
- package/dist/graphs/hyde.graph.d.ts +110 -0
- package/dist/graphs/hyde.graph.d.ts.map +1 -0
- package/dist/graphs/hyde.graph.js +235 -0
- package/dist/graphs/hyde.graph.js.map +1 -0
- package/dist/graphs/index.graph.d.ts +620 -0
- package/dist/graphs/index.graph.d.ts.map +1 -0
- package/dist/graphs/index.graph.js +226 -0
- package/dist/graphs/index.graph.js.map +1 -0
- package/dist/graphs/index_membership.graph.d.ts +250 -0
- package/dist/graphs/index_membership.graph.d.ts.map +1 -0
- package/dist/graphs/index_membership.graph.js +204 -0
- package/dist/graphs/index_membership.graph.js.map +1 -0
- package/dist/graphs/intent.graph.d.ts +490 -0
- package/dist/graphs/intent.graph.d.ts.map +1 -0
- package/dist/graphs/intent.graph.js +787 -0
- package/dist/graphs/intent.graph.js.map +1 -0
- package/dist/graphs/intent_index.graph.d.ts +396 -0
- package/dist/graphs/intent_index.graph.d.ts.map +1 -0
- package/dist/graphs/intent_index.graph.js +331 -0
- package/dist/graphs/intent_index.graph.js.map +1 -0
- package/dist/graphs/maintenance.graph.d.ts +177 -0
- package/dist/graphs/maintenance.graph.d.ts.map +1 -0
- package/dist/graphs/maintenance.graph.js +173 -0
- package/dist/graphs/maintenance.graph.js.map +1 -0
- package/dist/graphs/negotiation.graph.d.ts +819 -0
- package/dist/graphs/negotiation.graph.d.ts.map +1 -0
- package/dist/graphs/negotiation.graph.js +255 -0
- package/dist/graphs/negotiation.graph.js.map +1 -0
- package/dist/graphs/opportunity.graph.d.ts +1082 -0
- package/dist/graphs/opportunity.graph.d.ts.map +1 -0
- package/dist/graphs/opportunity.graph.js +2534 -0
- package/dist/graphs/opportunity.graph.js.map +1 -0
- package/dist/graphs/profile.graph.d.ts +617 -0
- package/dist/graphs/profile.graph.d.ts.map +1 -0
- package/dist/graphs/profile.graph.js +839 -0
- package/dist/graphs/profile.graph.js.map +1 -0
- package/dist/graphs/tests/chat.graph.mocks.d.ts +104 -0
- package/dist/graphs/tests/chat.graph.mocks.d.ts.map +1 -0
- package/dist/graphs/tests/chat.graph.mocks.js +225 -0
- package/dist/graphs/tests/chat.graph.mocks.js.map +1 -0
- package/dist/index.d.ts +62 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +44 -0
- package/dist/index.js.map +1 -0
- package/dist/interfaces/auth.interface.d.ts +15 -0
- package/dist/interfaces/auth.interface.d.ts.map +1 -0
- package/dist/interfaces/auth.interface.js +2 -0
- package/dist/interfaces/auth.interface.js.map +1 -0
- package/dist/interfaces/cache.interface.d.ts +43 -0
- package/dist/interfaces/cache.interface.d.ts.map +1 -0
- package/dist/interfaces/cache.interface.js +6 -0
- package/dist/interfaces/cache.interface.js.map +1 -0
- package/dist/interfaces/chat-session.interface.d.ts +11 -0
- package/dist/interfaces/chat-session.interface.d.ts.map +1 -0
- package/dist/interfaces/chat-session.interface.js +2 -0
- package/dist/interfaces/chat-session.interface.js.map +1 -0
- package/dist/interfaces/contact.interface.d.ts +48 -0
- package/dist/interfaces/contact.interface.d.ts.map +1 -0
- package/dist/interfaces/contact.interface.js +2 -0
- package/dist/interfaces/contact.interface.js.map +1 -0
- package/dist/interfaces/database.interface.d.ts +1495 -0
- package/dist/interfaces/database.interface.d.ts.map +1 -0
- package/dist/interfaces/database.interface.js +2 -0
- package/dist/interfaces/database.interface.js.map +1 -0
- package/dist/interfaces/embedder.interface.d.ts +85 -0
- package/dist/interfaces/embedder.interface.d.ts.map +1 -0
- package/dist/interfaces/embedder.interface.js +5 -0
- package/dist/interfaces/embedder.interface.js.map +1 -0
- package/dist/interfaces/enrichment.interface.d.ts +40 -0
- package/dist/interfaces/enrichment.interface.d.ts.map +1 -0
- package/dist/interfaces/enrichment.interface.js +2 -0
- package/dist/interfaces/enrichment.interface.js.map +1 -0
- package/dist/interfaces/integration.interface.d.ts +91 -0
- package/dist/interfaces/integration.interface.d.ts.map +1 -0
- package/dist/interfaces/integration.interface.js +2 -0
- package/dist/interfaces/integration.interface.js.map +1 -0
- package/dist/interfaces/queue.interface.d.ts +17 -0
- package/dist/interfaces/queue.interface.d.ts.map +1 -0
- package/dist/interfaces/queue.interface.js +5 -0
- package/dist/interfaces/queue.interface.js.map +1 -0
- package/dist/interfaces/scraper.interface.d.ts +31 -0
- package/dist/interfaces/scraper.interface.d.ts.map +1 -0
- package/dist/interfaces/scraper.interface.js +2 -0
- package/dist/interfaces/scraper.interface.js.map +1 -0
- package/dist/interfaces/storage.interface.d.ts +46 -0
- package/dist/interfaces/storage.interface.d.ts.map +1 -0
- package/dist/interfaces/storage.interface.js +6 -0
- package/dist/interfaces/storage.interface.js.map +1 -0
- package/dist/mcp/mcp.server.d.ts +29 -0
- package/dist/mcp/mcp.server.d.ts.map +1 -0
- package/dist/mcp/mcp.server.js +171 -0
- package/dist/mcp/mcp.server.js.map +1 -0
- package/dist/states/chat.state.d.ts +126 -0
- package/dist/states/chat.state.d.ts.map +1 -0
- package/dist/states/chat.state.js +112 -0
- package/dist/states/chat.state.js.map +1 -0
- package/dist/states/home.state.d.ts +100 -0
- package/dist/states/home.state.d.ts.map +1 -0
- package/dist/states/home.state.js +74 -0
- package/dist/states/home.state.js.map +1 -0
- package/dist/states/hyde.state.d.ts +54 -0
- package/dist/states/hyde.state.d.ts.map +1 -0
- package/dist/states/hyde.state.js +66 -0
- package/dist/states/hyde.state.js.map +1 -0
- package/dist/states/index.state.d.ts +179 -0
- package/dist/states/index.state.d.ts.map +1 -0
- package/dist/states/index.state.js +56 -0
- package/dist/states/index.state.js.map +1 -0
- package/dist/states/index_membership.state.d.ts +77 -0
- package/dist/states/index_membership.state.d.ts.map +1 -0
- package/dist/states/index_membership.state.js +43 -0
- package/dist/states/index_membership.state.js.map +1 -0
- package/dist/states/intent.state.d.ts +203 -0
- package/dist/states/intent.state.d.ts.map +1 -0
- package/dist/states/intent.state.js +153 -0
- package/dist/states/intent.state.js.map +1 -0
- package/dist/states/intent_index.state.d.ts +148 -0
- package/dist/states/intent_index.state.d.ts.map +1 -0
- package/dist/states/intent_index.state.js +100 -0
- package/dist/states/intent_index.state.js.map +1 -0
- package/dist/states/maintenance.state.d.ts +36 -0
- package/dist/states/maintenance.state.d.ts.map +1 -0
- package/dist/states/maintenance.state.js +56 -0
- package/dist/states/maintenance.state.js.map +1 -0
- package/dist/states/negotiation.state.d.ts +230 -0
- package/dist/states/negotiation.state.d.ts.map +1 -0
- package/dist/states/negotiation.state.js +82 -0
- package/dist/states/negotiation.state.js.map +1 -0
- package/dist/states/opportunity.state.d.ts +300 -0
- package/dist/states/opportunity.state.d.ts.map +1 -0
- package/dist/states/opportunity.state.js +207 -0
- package/dist/states/opportunity.state.js.map +1 -0
- package/dist/states/profile.state.d.ts +172 -0
- package/dist/states/profile.state.d.ts.map +1 -0
- package/dist/states/profile.state.js +133 -0
- package/dist/states/profile.state.js.map +1 -0
- package/dist/streamers/chat.streamer.d.ts +55 -0
- package/dist/streamers/chat.streamer.d.ts.map +1 -0
- package/dist/streamers/chat.streamer.js +186 -0
- package/dist/streamers/chat.streamer.js.map +1 -0
- package/dist/streamers/index.d.ts +3 -0
- package/dist/streamers/index.d.ts.map +1 -0
- package/dist/streamers/index.js +3 -0
- package/dist/streamers/index.js.map +1 -0
- package/dist/streamers/response.streamer.d.ts +36 -0
- package/dist/streamers/response.streamer.d.ts.map +1 -0
- package/dist/streamers/response.streamer.js +46 -0
- package/dist/streamers/response.streamer.js.map +1 -0
- package/dist/support/chat.utils.d.ts +42 -0
- package/dist/support/chat.utils.d.ts.map +1 -0
- package/dist/support/chat.utils.js +89 -0
- package/dist/support/chat.utils.js.map +1 -0
- package/dist/support/debug-meta.sanitizer.d.ts +18 -0
- package/dist/support/debug-meta.sanitizer.d.ts.map +1 -0
- package/dist/support/debug-meta.sanitizer.js +82 -0
- package/dist/support/debug-meta.sanitizer.js.map +1 -0
- package/dist/support/feed.health.d.ts +32 -0
- package/dist/support/feed.health.d.ts.map +1 -0
- package/dist/support/feed.health.js +76 -0
- package/dist/support/feed.health.js.map +1 -0
- package/dist/support/introducer.discovery.d.ts +78 -0
- package/dist/support/introducer.discovery.d.ts.map +1 -0
- package/dist/support/introducer.discovery.js +101 -0
- package/dist/support/introducer.discovery.js.map +1 -0
- package/dist/support/log.d.ts +65 -0
- package/dist/support/log.d.ts.map +1 -0
- package/dist/support/log.js +76 -0
- package/dist/support/log.js.map +1 -0
- package/dist/support/lucide.icon-catalog.d.ts +22 -0
- package/dist/support/lucide.icon-catalog.d.ts.map +1 -0
- package/dist/support/lucide.icon-catalog.js +101 -0
- package/dist/support/lucide.icon-catalog.js.map +1 -0
- package/dist/support/opportunity.card-text.d.ts +39 -0
- package/dist/support/opportunity.card-text.d.ts.map +1 -0
- package/dist/support/opportunity.card-text.js +333 -0
- package/dist/support/opportunity.card-text.js.map +1 -0
- package/dist/support/opportunity.constants.d.ts +9 -0
- package/dist/support/opportunity.constants.d.ts.map +1 -0
- package/dist/support/opportunity.constants.js +11 -0
- package/dist/support/opportunity.constants.js.map +1 -0
- package/dist/support/opportunity.discover.d.ts +144 -0
- package/dist/support/opportunity.discover.d.ts.map +1 -0
- package/dist/support/opportunity.discover.js +610 -0
- package/dist/support/opportunity.discover.js.map +1 -0
- package/dist/support/opportunity.enricher.d.ts +44 -0
- package/dist/support/opportunity.enricher.d.ts.map +1 -0
- package/dist/support/opportunity.enricher.js +245 -0
- package/dist/support/opportunity.enricher.js.map +1 -0
- package/dist/support/opportunity.persist.d.ts +39 -0
- package/dist/support/opportunity.persist.d.ts.map +1 -0
- package/dist/support/opportunity.persist.js +63 -0
- package/dist/support/opportunity.persist.js.map +1 -0
- package/dist/support/opportunity.presentation.d.ts +21 -0
- package/dist/support/opportunity.presentation.d.ts.map +1 -0
- package/dist/support/opportunity.presentation.js +75 -0
- package/dist/support/opportunity.presentation.js.map +1 -0
- package/dist/support/opportunity.sanitize.d.ts +18 -0
- package/dist/support/opportunity.sanitize.d.ts.map +1 -0
- package/dist/support/opportunity.sanitize.js +89 -0
- package/dist/support/opportunity.sanitize.js.map +1 -0
- package/dist/support/opportunity.utils.d.ts +99 -0
- package/dist/support/opportunity.utils.d.ts.map +1 -0
- package/dist/support/opportunity.utils.js +184 -0
- package/dist/support/opportunity.utils.js.map +1 -0
- package/dist/support/performance.d.ts +19 -0
- package/dist/support/performance.d.ts.map +1 -0
- package/dist/support/performance.js +43 -0
- package/dist/support/performance.js.map +1 -0
- package/dist/support/profile.enrichment-display-name.d.ts +16 -0
- package/dist/support/profile.enrichment-display-name.d.ts.map +1 -0
- package/dist/support/profile.enrichment-display-name.js +22 -0
- package/dist/support/profile.enrichment-display-name.js.map +1 -0
- package/dist/support/protocol.logger.d.ts +22 -0
- package/dist/support/protocol.logger.d.ts.map +1 -0
- package/dist/support/protocol.logger.js +44 -0
- package/dist/support/protocol.logger.js.map +1 -0
- package/dist/support/request-context.d.ts +19 -0
- package/dist/support/request-context.d.ts.map +1 -0
- package/dist/support/request-context.js +7 -0
- package/dist/support/request-context.js.map +1 -0
- package/dist/tools/contact.tools.d.ts +7 -0
- package/dist/tools/contact.tools.d.ts.map +1 -0
- package/dist/tools/contact.tools.js +115 -0
- package/dist/tools/contact.tools.js.map +1 -0
- package/dist/tools/index.d.ts +17 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +140 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/index.tools.d.ts +3 -0
- package/dist/tools/index.tools.d.ts.map +1 -0
- package/dist/tools/index.tools.js +423 -0
- package/dist/tools/index.tools.js.map +1 -0
- package/dist/tools/integration.tools.d.ts +13 -0
- package/dist/tools/integration.tools.d.ts.map +1 -0
- package/dist/tools/integration.tools.js +77 -0
- package/dist/tools/integration.tools.js.map +1 -0
- package/dist/tools/intent.tools.d.ts +3 -0
- package/dist/tools/intent.tools.d.ts.map +1 -0
- package/dist/tools/intent.tools.js +458 -0
- package/dist/tools/intent.tools.js.map +1 -0
- package/dist/tools/opportunity.tools.d.ts +44 -0
- package/dist/tools/opportunity.tools.d.ts.map +1 -0
- package/dist/tools/opportunity.tools.js +814 -0
- package/dist/tools/opportunity.tools.js.map +1 -0
- package/dist/tools/profile.tools.d.ts +3 -0
- package/dist/tools/profile.tools.d.ts.map +1 -0
- package/dist/tools/profile.tools.js +513 -0
- package/dist/tools/profile.tools.js.map +1 -0
- package/dist/tools/tool.helpers.d.ts +225 -0
- package/dist/tools/tool.helpers.d.ts.map +1 -0
- package/dist/tools/tool.helpers.js +172 -0
- package/dist/tools/tool.helpers.js.map +1 -0
- package/dist/tools/tool.registry.d.ts +12 -0
- package/dist/tools/tool.registry.d.ts.map +1 -0
- package/dist/tools/tool.registry.js +62 -0
- package/dist/tools/tool.registry.js.map +1 -0
- package/dist/tools/utility.tools.d.ts +3 -0
- package/dist/tools/utility.tools.d.ts.map +1 -0
- package/dist/tools/utility.tools.js +107 -0
- package/dist/tools/utility.tools.js.map +1 -0
- package/dist/types/chat-streaming.types.d.ts +472 -0
- package/dist/types/chat-streaming.types.d.ts.map +1 -0
- package/dist/types/chat-streaming.types.js +260 -0
- package/dist/types/chat-streaming.types.js.map +1 -0
- package/package.json +32 -0
|
@@ -0,0 +1,814 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { requestContext } from "../support/request-context.js";
|
|
3
|
+
import { success, error, UUID_REGEX } from "./tool.helpers.js";
|
|
4
|
+
import { MINIMAL_MAIN_TEXT_MAX_CHARS, getPrimaryActionLabel, SECONDARY_ACTION_LABEL } from "../support/opportunity.constants.js";
|
|
5
|
+
import { viewerCentricCardSummary, narratorRemarkFromReasoning } from "../support/opportunity.card-text.js";
|
|
6
|
+
import { runDiscoverFromQuery, continueDiscovery } from "../support/opportunity.discover.js";
|
|
7
|
+
import { protocolLogger } from "../support/protocol.logger.js";
|
|
8
|
+
const logger = protocolLogger("ChatTools:Opportunity");
|
|
9
|
+
/** Maximum number of opportunity cards to show per chat response. */
|
|
10
|
+
const CHAT_DISPLAY_LIMIT = 3;
|
|
11
|
+
/** Markdown code fence (three backticks). Avoids embedding ``` in string literals so TS parser stays in sync. */
|
|
12
|
+
const CODE_FENCE = String.fromCharCode(96, 96, 96);
|
|
13
|
+
/**
|
|
14
|
+
* Sanitize JSON string for use inside a markdown code fence (```). Escapes backticks
|
|
15
|
+
* so embedded ``` cannot close the fence prematurely.
|
|
16
|
+
*/
|
|
17
|
+
function sanitizeJsonForCodeFence(json) {
|
|
18
|
+
return json.replace(/`/g, "\\u0060");
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Build minimal opportunity card data for chat without calling the LLM presenter.
|
|
22
|
+
* Uses only required fields from the opportunity record and counterpart name/avatar
|
|
23
|
+
* so list_opportunities and discovery return quickly.
|
|
24
|
+
*
|
|
25
|
+
* Note: narratorChip.text is generated via regex heuristics (narratorRemarkFromReasoning)
|
|
26
|
+
* rather than the OpportunityPresenter LLM. If narrator quality becomes an issue again,
|
|
27
|
+
* consider making this function async and delegating to OpportunityPresenter.presentHomeCard()
|
|
28
|
+
* which already produces a high-quality narratorRemark via LLM (used by the home graph
|
|
29
|
+
* and discovery pipeline). The trade-off is 5-20s latency per card.
|
|
30
|
+
*
|
|
31
|
+
* Exported for use in tests (opportunity.tools.spec.ts).
|
|
32
|
+
*/
|
|
33
|
+
export function buildMinimalOpportunityCard(opp, viewerId, counterpartUserId, counterpartName, counterpartAvatar, introducerName, introducerAvatar, viewerName, secondPartyName, secondPartyAvatar, secondPartyUserId, isCounterpartGhost) {
|
|
34
|
+
const viewerActor = opp.actors.find((a) => a.userId === viewerId);
|
|
35
|
+
const viewerRole = viewerActor?.role ?? "party";
|
|
36
|
+
const introducerActor = opp.actors.find((a) => a.role === "introducer" && a.userId !== viewerId);
|
|
37
|
+
const viewerIsIntroducer = opp.actors.some((a) => a.role === "introducer" && a.userId === viewerId);
|
|
38
|
+
const reasoning = opp.interpretation?.reasoning ?? "";
|
|
39
|
+
const mainText = viewerCentricCardSummary(reasoning, counterpartName, MINIMAL_MAIN_TEXT_MAX_CHARS, viewerName, introducerName ?? undefined);
|
|
40
|
+
const score = typeof opp.interpretation?.confidence === "number"
|
|
41
|
+
? opp.interpretation.confidence
|
|
42
|
+
: undefined;
|
|
43
|
+
const narratorName = viewerIsIntroducer
|
|
44
|
+
? "You"
|
|
45
|
+
: introducerName?.trim() || (introducerActor ? "Someone" : "Index");
|
|
46
|
+
const primaryActionLabel = getPrimaryActionLabel(viewerRole);
|
|
47
|
+
return {
|
|
48
|
+
opportunityId: opp.id,
|
|
49
|
+
userId: counterpartUserId,
|
|
50
|
+
name: counterpartName,
|
|
51
|
+
avatar: counterpartAvatar,
|
|
52
|
+
mainText,
|
|
53
|
+
cta: "Start a conversation to connect.",
|
|
54
|
+
headline: viewerIsIntroducer && secondPartyName
|
|
55
|
+
? `${counterpartName} → ${secondPartyName}`
|
|
56
|
+
: `Connection with ${counterpartName}`,
|
|
57
|
+
primaryActionLabel,
|
|
58
|
+
secondaryActionLabel: SECONDARY_ACTION_LABEL,
|
|
59
|
+
mutualIntentsLabel: "Suggested connection",
|
|
60
|
+
narratorChip: {
|
|
61
|
+
name: narratorName,
|
|
62
|
+
text: narratorRemarkFromReasoning(reasoning, counterpartName, viewerName),
|
|
63
|
+
...(viewerIsIntroducer
|
|
64
|
+
? { userId: viewerId, avatar: null }
|
|
65
|
+
: introducerActor
|
|
66
|
+
? { userId: introducerActor.userId, avatar: introducerAvatar ?? null }
|
|
67
|
+
: {}),
|
|
68
|
+
},
|
|
69
|
+
viewerRole,
|
|
70
|
+
score,
|
|
71
|
+
status: opp.status ?? "latent",
|
|
72
|
+
isGhost: isCounterpartGhost ?? false,
|
|
73
|
+
...(viewerIsIntroducer && secondPartyName
|
|
74
|
+
? {
|
|
75
|
+
secondParty: {
|
|
76
|
+
name: secondPartyName,
|
|
77
|
+
...(secondPartyAvatar != null ? { avatar: secondPartyAvatar } : {}),
|
|
78
|
+
...(secondPartyUserId ? { userId: secondPartyUserId } : {}),
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
: {}),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
export function createOpportunityTools(defineTool, deps) {
|
|
85
|
+
const { database, userDb, systemDb, graphs, embedder, cache } = deps;
|
|
86
|
+
const createOpportunities = defineTool({
|
|
87
|
+
name: "create_opportunities",
|
|
88
|
+
description: "Creates opportunities (connections). NOT for looking up a specific person by name — use read_user_profiles(query=name) for that.\n\n" +
|
|
89
|
+
"Four modes:\n" +
|
|
90
|
+
"1. **Discovery**: pass searchQuery and/or indexId. Finds matching people based on intent overlap.\n" +
|
|
91
|
+
"2. **Introduction**: pass partyUserIds (2+ user IDs) + entities (pre-gathered profiles and intents). " +
|
|
92
|
+
"You MUST gather profiles and intents from shared indexes BEFORE calling this. " +
|
|
93
|
+
"Optionally pass hint (the user's reason for the introduction).\n" +
|
|
94
|
+
"3. **Direct connection**: pass targetUserId (a single user ID) + searchQuery (reason for connecting). " +
|
|
95
|
+
"Creates an opportunity between the current user and the target user.\n" +
|
|
96
|
+
"4. **Introducer discovery**: pass introTargetUserId (user ID to find matches FOR). " +
|
|
97
|
+
"Discovers matches for that person; current user becomes the introducer. " +
|
|
98
|
+
"Use when user asks 'who should I introduce to @Person'.\n\n" +
|
|
99
|
+
"Results are saved as drafts; use update_opportunity(status='pending') to send.",
|
|
100
|
+
querySchema: z.object({
|
|
101
|
+
continueFrom: z
|
|
102
|
+
.string()
|
|
103
|
+
.optional()
|
|
104
|
+
.describe("Discovery pagination: pass the discoveryId from a previous result to evaluate more candidates."),
|
|
105
|
+
searchQuery: z
|
|
106
|
+
.string()
|
|
107
|
+
.optional()
|
|
108
|
+
.describe("Discovery mode: what to look for."),
|
|
109
|
+
indexId: z
|
|
110
|
+
.string()
|
|
111
|
+
.optional()
|
|
112
|
+
.describe("Index UUID; optional when index-scoped. Pass the personal index ID (\"My Network\") to scope discovery to the user's contacts only."),
|
|
113
|
+
intentId: z
|
|
114
|
+
.string()
|
|
115
|
+
.optional()
|
|
116
|
+
.describe("Discovery mode: optional intent to use as source and for triggeredBy (e.g. from queue)."),
|
|
117
|
+
targetUserId: z
|
|
118
|
+
.string()
|
|
119
|
+
.optional()
|
|
120
|
+
.describe("Direct connection mode: create opportunity with this specific user ID. Used when the user wants to connect with a named person."),
|
|
121
|
+
introTargetUserId: z
|
|
122
|
+
.string()
|
|
123
|
+
.optional()
|
|
124
|
+
.describe("Introducer discovery mode: find matches FOR this user ID (the current user becomes the introducer). " +
|
|
125
|
+
"Use when the user asks 'who should I introduce to @Person'. " +
|
|
126
|
+
"Do NOT combine with partyUserIds (that's full introduction mode)."),
|
|
127
|
+
partyUserIds: z
|
|
128
|
+
.array(z.string())
|
|
129
|
+
.optional()
|
|
130
|
+
.describe("Introduction mode: user IDs to introduce (at least 2)."),
|
|
131
|
+
entities: z
|
|
132
|
+
.array(z.object({
|
|
133
|
+
userId: z.string(),
|
|
134
|
+
profile: z
|
|
135
|
+
.object({
|
|
136
|
+
name: z.string().optional(),
|
|
137
|
+
bio: z.string().optional(),
|
|
138
|
+
location: z.string().optional(),
|
|
139
|
+
interests: z.array(z.string()).optional(),
|
|
140
|
+
skills: z.array(z.string()).optional(),
|
|
141
|
+
context: z.string().optional(),
|
|
142
|
+
})
|
|
143
|
+
.optional(),
|
|
144
|
+
intents: z
|
|
145
|
+
.array(z.object({
|
|
146
|
+
intentId: z.string(),
|
|
147
|
+
payload: z.string(),
|
|
148
|
+
summary: z.string().optional(),
|
|
149
|
+
}))
|
|
150
|
+
.optional(),
|
|
151
|
+
indexId: z
|
|
152
|
+
.string()
|
|
153
|
+
.describe("Shared index this entity's data comes from (required for intro mode)"),
|
|
154
|
+
}))
|
|
155
|
+
.optional()
|
|
156
|
+
.describe("Introduction mode: pre-gathered profiles + intents per party. Gather via read_user_profiles + read_intents before calling."),
|
|
157
|
+
hint: z
|
|
158
|
+
.string()
|
|
159
|
+
.optional()
|
|
160
|
+
.describe("Introduction mode: the user's reason for the intro (e.g. 'both AI devs')."),
|
|
161
|
+
}),
|
|
162
|
+
handler: async ({ context, query }) => {
|
|
163
|
+
// Strict scope enforcement: when chat is index-scoped, only allow that index
|
|
164
|
+
if (context.indexId &&
|
|
165
|
+
query.indexId?.trim() &&
|
|
166
|
+
query.indexId.trim() !== context.indexId) {
|
|
167
|
+
return error(`This chat is scoped to ${context.indexName ?? "this index"}. You can only create opportunities in this community.`);
|
|
168
|
+
}
|
|
169
|
+
const effectiveIndexId = (context.indexId || query.indexId?.trim()) ?? undefined;
|
|
170
|
+
// ── Continuation mode ── (must take strict precedence — it's a pagination token)
|
|
171
|
+
if (query.continueFrom) {
|
|
172
|
+
const _continueTraceEmitter = requestContext.getStore()?.traceEmitter;
|
|
173
|
+
const _graphStart = Date.now();
|
|
174
|
+
_continueTraceEmitter?.({ type: "graph_start", name: "opportunity" });
|
|
175
|
+
const result = await continueDiscovery({
|
|
176
|
+
opportunityGraph: graphs.opportunity,
|
|
177
|
+
database,
|
|
178
|
+
cache,
|
|
179
|
+
userId: context.userId,
|
|
180
|
+
discoveryId: query.continueFrom,
|
|
181
|
+
expectedIndexId: context.indexId,
|
|
182
|
+
limit: 20,
|
|
183
|
+
minimalForChat: true,
|
|
184
|
+
...(context.sessionId ? { chatSessionId: context.sessionId } : {}),
|
|
185
|
+
});
|
|
186
|
+
const _graphMs = Date.now() - _graphStart;
|
|
187
|
+
_continueTraceEmitter?.({ type: "graph_end", name: "opportunity", durationMs: _graphMs });
|
|
188
|
+
const allDebugSteps = [...(result.debugSteps ?? [])];
|
|
189
|
+
if (!result.found) {
|
|
190
|
+
return success({
|
|
191
|
+
found: false,
|
|
192
|
+
count: 0,
|
|
193
|
+
message: result.message ?? "No more matching opportunities found in the remaining candidates.",
|
|
194
|
+
summary: "No more matches found",
|
|
195
|
+
...(result.pagination ? { pagination: result.pagination } : {}),
|
|
196
|
+
debugSteps: allDebugSteps,
|
|
197
|
+
_graphTimings: [{ name: 'opportunity', durationMs: _graphMs, agents: [] }],
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
// Format opportunity blocks — same pattern as the discovery path below
|
|
201
|
+
const opportunityBlocks = (result.opportunities ?? []).map((opp) => {
|
|
202
|
+
const cardData = {
|
|
203
|
+
opportunityId: opp.opportunityId,
|
|
204
|
+
userId: opp.userId,
|
|
205
|
+
name: opp.name,
|
|
206
|
+
avatar: opp.avatar,
|
|
207
|
+
mainText: opp.homeCardPresentation?.personalizedSummary ?? opp.matchReason ?? "",
|
|
208
|
+
cta: opp.homeCardPresentation?.suggestedAction,
|
|
209
|
+
headline: opp.homeCardPresentation?.headline,
|
|
210
|
+
primaryActionLabel: opp.homeCardPresentation?.primaryActionLabel,
|
|
211
|
+
secondaryActionLabel: opp.homeCardPresentation?.secondaryActionLabel,
|
|
212
|
+
mutualIntentsLabel: opp.homeCardPresentation?.mutualIntentsLabel,
|
|
213
|
+
narratorChip: opp.narratorChip,
|
|
214
|
+
viewerRole: opp.viewerRole,
|
|
215
|
+
isGhost: opp.isGhost ?? false,
|
|
216
|
+
score: opp.score,
|
|
217
|
+
status: opp.status,
|
|
218
|
+
};
|
|
219
|
+
return (CODE_FENCE + "opportunity\n" +
|
|
220
|
+
sanitizeJsonForCodeFence(JSON.stringify(cardData)) +
|
|
221
|
+
"\n" + CODE_FENCE);
|
|
222
|
+
});
|
|
223
|
+
// Cap displayed cards at CHAT_DISPLAY_LIMIT; remaining feed into pagination
|
|
224
|
+
const displayedBlocks = opportunityBlocks.slice(0, CHAT_DISPLAY_LIMIT);
|
|
225
|
+
const extraFromCap = opportunityBlocks.length - displayedBlocks.length;
|
|
226
|
+
const blocksText = displayedBlocks.join("\n\n");
|
|
227
|
+
let message = "Found " + displayedBlocks.length + " more potential connection(s). IMPORTANT: Include the following opportunity code blocks EXACTLY as-is in your response (they render as interactive cards):\n\n" +
|
|
228
|
+
blocksText;
|
|
229
|
+
const isIntroducerContinuation = !!query.introTargetUserId?.trim();
|
|
230
|
+
const totalRemaining = (result.pagination?.remaining ?? 0) + extraFromCap;
|
|
231
|
+
if (totalRemaining > 0 && result.pagination?.discoveryId) {
|
|
232
|
+
message += `\n\nThere are ${totalRemaining} more candidates. Ask if the user wants to see more — they can say "show me more" and you should call create_opportunities with continueFrom="${result.pagination.discoveryId}".`;
|
|
233
|
+
}
|
|
234
|
+
else if (isIntroducerContinuation) {
|
|
235
|
+
message += `\n\nThese are all the introduction candidates I found for this person.`;
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
message += `\n\nThese are all the connections I found. If the user wants to attract more connections, suggest they create a signal — e.g. "Would you like to create a signal so others looking for someone like you can find you?" If they agree, call create_intent with a description based on what they were searching for.`;
|
|
239
|
+
}
|
|
240
|
+
return success({
|
|
241
|
+
found: true,
|
|
242
|
+
count: displayedBlocks.length,
|
|
243
|
+
message,
|
|
244
|
+
summary: `Found ${displayedBlocks.length} more match(es)`,
|
|
245
|
+
...(result.pagination ? { pagination: result.pagination } : {}),
|
|
246
|
+
debugSteps: allDebugSteps,
|
|
247
|
+
_graphTimings: [{ name: 'opportunity', durationMs: _graphMs, agents: [] }],
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
// Derive partyUserIds from entities when agent passes entities but omits partyUserIds (intro mode).
|
|
251
|
+
// Only derive when all entities share the same indexId to prevent cross-index introductions.
|
|
252
|
+
const partyUserIdsFromEntities = query.entities &&
|
|
253
|
+
query.entities.length >= 2 &&
|
|
254
|
+
query.entities.every((e) => e.userId && e.indexId) &&
|
|
255
|
+
new Set(query.entities.map((e) => e.indexId)).size === 1
|
|
256
|
+
? [...new Set(query.entities.map((e) => e.userId))]
|
|
257
|
+
: undefined;
|
|
258
|
+
const effectivePartyUserIds = query.partyUserIds && query.partyUserIds.length >= 2
|
|
259
|
+
? query.partyUserIds
|
|
260
|
+
: (partyUserIdsFromEntities?.length ?? 0) >= 2
|
|
261
|
+
? partyUserIdsFromEntities
|
|
262
|
+
: undefined;
|
|
263
|
+
// ── Introduction mode ── (validation and persistence via opportunity graph)
|
|
264
|
+
if (effectivePartyUserIds && effectivePartyUserIds.length >= 2) {
|
|
265
|
+
if (!query.entities || query.entities.length === 0) {
|
|
266
|
+
return error("Introduction requires pre-gathered entity data. " +
|
|
267
|
+
"First use read_index_memberships to find shared indexes, " +
|
|
268
|
+
"then read_user_profiles and read_intents for each party, " +
|
|
269
|
+
"then pass the results as entities.");
|
|
270
|
+
}
|
|
271
|
+
const primaryIndexId = query.entities[0]?.indexId;
|
|
272
|
+
if (!primaryIndexId) {
|
|
273
|
+
return error("Each entity must include an indexId (the shared index).");
|
|
274
|
+
}
|
|
275
|
+
const introducedPartyUserIds = effectivePartyUserIds.filter((uid) => uid !== context.userId);
|
|
276
|
+
if (introducedPartyUserIds.length === 0) {
|
|
277
|
+
return error("No counterpart to introduce. Provide at least one other user ID in partyUserIds (besides yourself).");
|
|
278
|
+
}
|
|
279
|
+
const evaluatorEntities = query.entities.map((e) => ({
|
|
280
|
+
userId: e.userId,
|
|
281
|
+
profile: e.profile ?? {},
|
|
282
|
+
intents: e.intents,
|
|
283
|
+
indexId: e.indexId,
|
|
284
|
+
}));
|
|
285
|
+
const _introGraphStart = Date.now();
|
|
286
|
+
const _introTraceEmitter = requestContext.getStore()?.traceEmitter;
|
|
287
|
+
_introTraceEmitter?.({ type: "graph_start", name: "opportunity" });
|
|
288
|
+
const result = await graphs.opportunity.invoke({
|
|
289
|
+
operationMode: "create_introduction",
|
|
290
|
+
userId: context.userId,
|
|
291
|
+
indexId: primaryIndexId,
|
|
292
|
+
introductionEntities: evaluatorEntities,
|
|
293
|
+
introductionHint: query.hint,
|
|
294
|
+
requiredIndexId: context.indexId ?? undefined,
|
|
295
|
+
options: {
|
|
296
|
+
initialStatus: "draft",
|
|
297
|
+
...(context.sessionId ? { conversationId: context.sessionId } : {}),
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
const _introGraphMs = Date.now() - _introGraphStart;
|
|
301
|
+
_introTraceEmitter?.({ type: "graph_end", name: "opportunity", durationMs: _introGraphMs });
|
|
302
|
+
if (result.error || !result.opportunities?.length) {
|
|
303
|
+
return error(result.error ?? "Failed to create introduction.");
|
|
304
|
+
}
|
|
305
|
+
const created = result.opportunities[0];
|
|
306
|
+
const reasoning = created.interpretation?.reasoning ?? "A suggested connection.";
|
|
307
|
+
const confidence = typeof created.interpretation?.confidence === "number"
|
|
308
|
+
? created.interpretation.confidence
|
|
309
|
+
: parseFloat(String(created.confidence ?? 0)) || 0;
|
|
310
|
+
const introducerUser = await userDb.getUser();
|
|
311
|
+
const firstPartyId = introducedPartyUserIds[0];
|
|
312
|
+
const firstEntity = query.entities?.find((e) => e.userId === firstPartyId);
|
|
313
|
+
const counterpartUser = firstPartyId
|
|
314
|
+
? await database.getUser(firstPartyId)
|
|
315
|
+
: null;
|
|
316
|
+
const counterpartName = firstEntity?.profile?.name ?? firstPartyId ?? "Someone";
|
|
317
|
+
// Second party — used in the headline and arrow layout for the introducer view ("A → B")
|
|
318
|
+
const secondPartyId = introducedPartyUserIds[1];
|
|
319
|
+
const secondEntity = query.entities?.find((e) => e.userId === secondPartyId);
|
|
320
|
+
const secondPartyName = secondEntity?.profile?.name;
|
|
321
|
+
const secondPartyAvatar = secondEntity?.profile?.avatar ?? null;
|
|
322
|
+
const secondPartyUser = secondPartyId ? await database.getUser(secondPartyId) : null;
|
|
323
|
+
const viewerIsParty = effectivePartyUserIds.includes(context.userId);
|
|
324
|
+
const viewerRole = viewerIsParty ? "party" : "introducer";
|
|
325
|
+
const isCounterpartGhost = counterpartUser?.isGhost ?? false;
|
|
326
|
+
const primaryActionLabel = getPrimaryActionLabel(viewerRole);
|
|
327
|
+
const narratorChip = viewerIsParty
|
|
328
|
+
? {
|
|
329
|
+
name: "Index",
|
|
330
|
+
text: narratorRemarkFromReasoning(reasoning, counterpartName, introducerUser?.name ?? undefined),
|
|
331
|
+
}
|
|
332
|
+
: {
|
|
333
|
+
name: "You",
|
|
334
|
+
text: narratorRemarkFromReasoning(reasoning, counterpartName, introducerUser?.name ?? undefined),
|
|
335
|
+
userId: context.userId,
|
|
336
|
+
};
|
|
337
|
+
const headline = !viewerIsParty && secondPartyName
|
|
338
|
+
? `${counterpartName} → ${secondPartyName}`
|
|
339
|
+
: `Connection with ${counterpartName}`;
|
|
340
|
+
const cardData = {
|
|
341
|
+
opportunityId: created.id,
|
|
342
|
+
userId: firstPartyId,
|
|
343
|
+
name: counterpartName,
|
|
344
|
+
avatar: counterpartUser?.avatar ??
|
|
345
|
+
firstEntity?.profile
|
|
346
|
+
?.avatar ??
|
|
347
|
+
null,
|
|
348
|
+
mainText: viewerCentricCardSummary(reasoning, counterpartName, MINIMAL_MAIN_TEXT_MAX_CHARS, undefined, // viewerName not available in this context; introducer name passed separately
|
|
349
|
+
introducerUser?.name ?? undefined),
|
|
350
|
+
cta: "Start a conversation to connect.",
|
|
351
|
+
headline,
|
|
352
|
+
primaryActionLabel,
|
|
353
|
+
secondaryActionLabel: SECONDARY_ACTION_LABEL,
|
|
354
|
+
mutualIntentsLabel: "Suggested connection",
|
|
355
|
+
narratorChip,
|
|
356
|
+
viewerRole,
|
|
357
|
+
isGhost: isCounterpartGhost,
|
|
358
|
+
score: confidence,
|
|
359
|
+
status: created.status ?? "draft",
|
|
360
|
+
...(!viewerIsParty && secondPartyName
|
|
361
|
+
? {
|
|
362
|
+
secondParty: {
|
|
363
|
+
name: secondPartyName,
|
|
364
|
+
avatar: secondPartyUser?.avatar ?? secondPartyAvatar,
|
|
365
|
+
...(secondPartyId ? { userId: secondPartyId } : {}),
|
|
366
|
+
},
|
|
367
|
+
}
|
|
368
|
+
: {}),
|
|
369
|
+
};
|
|
370
|
+
const block = CODE_FENCE + "opportunity\n" +
|
|
371
|
+
sanitizeJsonForCodeFence(JSON.stringify(cardData)) +
|
|
372
|
+
"\n" + CODE_FENCE;
|
|
373
|
+
return success({
|
|
374
|
+
found: true,
|
|
375
|
+
count: 1,
|
|
376
|
+
summary: "Draft introduction created",
|
|
377
|
+
message: "Draft introduction created. IMPORTANT: Include the following " +
|
|
378
|
+
CODE_FENCE +
|
|
379
|
+
"opportunity code block EXACTLY as-is in your response (it renders as an interactive card):\n\n" +
|
|
380
|
+
block,
|
|
381
|
+
opportunities: [
|
|
382
|
+
{
|
|
383
|
+
opportunityId: created.id,
|
|
384
|
+
matchReason: reasoning,
|
|
385
|
+
score: confidence,
|
|
386
|
+
status: created.status ?? "draft",
|
|
387
|
+
},
|
|
388
|
+
],
|
|
389
|
+
_graphTimings: [{ name: 'opportunity', durationMs: _introGraphMs, agents: result.agentTimings ?? [] }],
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
// ── Discovery mode ──
|
|
393
|
+
const searchQuery = query.searchQuery?.trim() ?? "";
|
|
394
|
+
if (query.intentId != null && query.intentId !== "" && !UUID_REGEX.test(query.intentId.trim())) {
|
|
395
|
+
return error("Invalid intent ID format.");
|
|
396
|
+
}
|
|
397
|
+
let indexScope;
|
|
398
|
+
const _scopeGraphTimings = [];
|
|
399
|
+
if (effectiveIndexId) {
|
|
400
|
+
if (!UUID_REGEX.test(effectiveIndexId)) {
|
|
401
|
+
return error("Invalid index ID format.");
|
|
402
|
+
}
|
|
403
|
+
const _scopeGraphStart = Date.now();
|
|
404
|
+
const _scopeIndexMembershipTraceEmitter = requestContext.getStore()?.traceEmitter;
|
|
405
|
+
_scopeIndexMembershipTraceEmitter?.({ type: "graph_start", name: "index_membership" });
|
|
406
|
+
const memberResult = await graphs.indexMembership.invoke({
|
|
407
|
+
userId: context.userId,
|
|
408
|
+
indexId: effectiveIndexId,
|
|
409
|
+
operationMode: "read",
|
|
410
|
+
});
|
|
411
|
+
const _scopeIndexMembershipMs = Date.now() - _scopeGraphStart;
|
|
412
|
+
_scopeIndexMembershipTraceEmitter?.({ type: "graph_end", name: "index_membership", durationMs: _scopeIndexMembershipMs });
|
|
413
|
+
_scopeGraphTimings.push({ name: 'index_membership', durationMs: _scopeIndexMembershipMs, agents: [] });
|
|
414
|
+
if (memberResult.error) {
|
|
415
|
+
return error("Index not found or you are not a member.");
|
|
416
|
+
}
|
|
417
|
+
indexScope = [effectiveIndexId];
|
|
418
|
+
}
|
|
419
|
+
else if (context.indexId) {
|
|
420
|
+
// When scoped but no explicit indexId, use the scoped index
|
|
421
|
+
indexScope = [context.indexId];
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
// No scope - use all indexes (only in unscoped chat)
|
|
425
|
+
const _scopeGraphStart = Date.now();
|
|
426
|
+
const _scopeIndexTraceEmitter = requestContext.getStore()?.traceEmitter;
|
|
427
|
+
_scopeIndexTraceEmitter?.({ type: "graph_start", name: "index" });
|
|
428
|
+
const indexResult = await graphs.index.invoke({
|
|
429
|
+
userId: context.userId,
|
|
430
|
+
operationMode: "read",
|
|
431
|
+
showAll: true,
|
|
432
|
+
});
|
|
433
|
+
const _scopeIndexMs = Date.now() - _scopeGraphStart;
|
|
434
|
+
_scopeIndexTraceEmitter?.({ type: "graph_end", name: "index", durationMs: _scopeIndexMs });
|
|
435
|
+
_scopeGraphTimings.push({ name: 'index', durationMs: _scopeIndexMs, agents: [] });
|
|
436
|
+
indexScope = (indexResult.readResult?.memberOf || []).map((m) => m.indexId);
|
|
437
|
+
}
|
|
438
|
+
const toolDebugSteps = [
|
|
439
|
+
{ step: "resolve_index_scope", detail: `${indexScope.length} index(es)` },
|
|
440
|
+
];
|
|
441
|
+
const triggerIntentId = query.intentId?.trim() || undefined;
|
|
442
|
+
if (triggerIntentId != null && !UUID_REGEX.test(triggerIntentId)) {
|
|
443
|
+
return error("Invalid intent ID format.");
|
|
444
|
+
}
|
|
445
|
+
if (query.introTargetUserId?.trim() && query.introTargetUserId.trim() === context.userId) {
|
|
446
|
+
return error("You cannot discover introductions for yourself. Try regular discovery instead.");
|
|
447
|
+
}
|
|
448
|
+
const _discoverTraceEmitter = requestContext.getStore()?.traceEmitter;
|
|
449
|
+
const _discoverGraphStart = Date.now();
|
|
450
|
+
_discoverTraceEmitter?.({ type: "graph_start", name: "opportunity" });
|
|
451
|
+
const result = await runDiscoverFromQuery({
|
|
452
|
+
opportunityGraph: graphs.opportunity,
|
|
453
|
+
database,
|
|
454
|
+
userId: context.userId,
|
|
455
|
+
query: searchQuery,
|
|
456
|
+
indexScope,
|
|
457
|
+
limit: 20,
|
|
458
|
+
minimalForChat: true, // Skip LLM presenter; return only required fields for fast chat
|
|
459
|
+
triggerIntentId,
|
|
460
|
+
targetUserId: query.targetUserId?.trim() || undefined,
|
|
461
|
+
onBehalfOfUserId: query.introTargetUserId?.trim() || undefined,
|
|
462
|
+
cache,
|
|
463
|
+
...(context.sessionId ? { chatSessionId: context.sessionId } : {}),
|
|
464
|
+
});
|
|
465
|
+
const _discoverGraphMs = Date.now() - _discoverGraphStart;
|
|
466
|
+
_discoverTraceEmitter?.({ type: "graph_end", name: "opportunity", durationMs: _discoverGraphMs });
|
|
467
|
+
const _discoverGraphTimings = [
|
|
468
|
+
..._scopeGraphTimings,
|
|
469
|
+
{ name: 'opportunity', durationMs: _discoverGraphMs, agents: [] },
|
|
470
|
+
];
|
|
471
|
+
const allDebugSteps = [
|
|
472
|
+
...toolDebugSteps,
|
|
473
|
+
...(result.debugSteps ?? []),
|
|
474
|
+
];
|
|
475
|
+
const isIntroducerFlow = !!query.introTargetUserId?.trim();
|
|
476
|
+
if (result.createIntentSuggested && result.suggestedIntentDescription && !isIntroducerFlow) {
|
|
477
|
+
return success({
|
|
478
|
+
found: false,
|
|
479
|
+
count: 0,
|
|
480
|
+
createIntentSuggested: true,
|
|
481
|
+
suggestedIntentDescription: result.suggestedIntentDescription,
|
|
482
|
+
message: "No matching opportunities found. Call create_intent with the suggested description, then create_opportunities again.",
|
|
483
|
+
summary: "No matches found",
|
|
484
|
+
...(result.pagination ? { pagination: result.pagination } : {}),
|
|
485
|
+
debugSteps: allDebugSteps,
|
|
486
|
+
_graphTimings: _discoverGraphTimings,
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
if (!result.found) {
|
|
490
|
+
return success({
|
|
491
|
+
found: false,
|
|
492
|
+
count: 0,
|
|
493
|
+
message: result.message ?? "No matching opportunities found.",
|
|
494
|
+
summary: "No matches found",
|
|
495
|
+
...(result.pagination ? { pagination: result.pagination } : {}),
|
|
496
|
+
debugSteps: allDebugSteps,
|
|
497
|
+
_graphTimings: _discoverGraphTimings,
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
// Found but only existing connections (no new opportunities created)
|
|
501
|
+
const forMention = result.existingConnectionsForMention ?? result.existingConnections ?? [];
|
|
502
|
+
if ((result.opportunities?.length ?? 0) === 0 && forMention.length > 0) {
|
|
503
|
+
return success({
|
|
504
|
+
found: true,
|
|
505
|
+
count: 0,
|
|
506
|
+
message: result.message ??
|
|
507
|
+
"No new opportunities created; you already have a connection with: " +
|
|
508
|
+
forMention.map((c) => c.name + (c.status ? " (" + c.status + ")" : "")).join(", ") +
|
|
509
|
+
". View on your home page.",
|
|
510
|
+
existingConnections: result.existingConnections,
|
|
511
|
+
summary: "No new matches (existing connections only)",
|
|
512
|
+
debugSteps: allDebugSteps,
|
|
513
|
+
_graphTimings: _discoverGraphTimings,
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
// Format opportunities as code blocks for the LLM to include in its response
|
|
517
|
+
// The frontend will parse opportunity code blocks and render them as cards
|
|
518
|
+
const opportunityBlocks = (result.opportunities ?? []).map((opp) => {
|
|
519
|
+
const cardData = {
|
|
520
|
+
opportunityId: opp.opportunityId,
|
|
521
|
+
userId: opp.userId,
|
|
522
|
+
name: opp.name,
|
|
523
|
+
avatar: opp.avatar,
|
|
524
|
+
mainText: opp.homeCardPresentation?.personalizedSummary ??
|
|
525
|
+
opp.matchReason ??
|
|
526
|
+
"",
|
|
527
|
+
cta: opp.homeCardPresentation?.suggestedAction,
|
|
528
|
+
headline: opp.homeCardPresentation?.headline,
|
|
529
|
+
primaryActionLabel: opp.homeCardPresentation?.primaryActionLabel,
|
|
530
|
+
secondaryActionLabel: opp.homeCardPresentation?.secondaryActionLabel,
|
|
531
|
+
mutualIntentsLabel: opp.homeCardPresentation?.mutualIntentsLabel,
|
|
532
|
+
narratorChip: opp.narratorChip,
|
|
533
|
+
viewerRole: opp.viewerRole,
|
|
534
|
+
isGhost: opp.isGhost ?? false,
|
|
535
|
+
score: opp.score,
|
|
536
|
+
status: opp.status,
|
|
537
|
+
...(opp.secondParty && { secondParty: opp.secondParty }),
|
|
538
|
+
};
|
|
539
|
+
return (CODE_FENCE + "opportunity\n" +
|
|
540
|
+
sanitizeJsonForCodeFence(JSON.stringify(cardData)) +
|
|
541
|
+
"\n" + CODE_FENCE);
|
|
542
|
+
});
|
|
543
|
+
// Cap displayed cards at CHAT_DISPLAY_LIMIT; remaining feed into pagination
|
|
544
|
+
const displayedBlocks = opportunityBlocks.slice(0, CHAT_DISPLAY_LIMIT);
|
|
545
|
+
const extraFromCap = opportunityBlocks.length - displayedBlocks.length;
|
|
546
|
+
// Join all opportunity blocks into a single string for the LLM to include verbatim
|
|
547
|
+
const blocksText = displayedBlocks.join("\n\n");
|
|
548
|
+
let message = "Found " +
|
|
549
|
+
displayedBlocks.length +
|
|
550
|
+
" potential connection(s). IMPORTANT: Include the following " + CODE_FENCE + "opportunity code blocks EXACTLY as-is in your response (they render as interactive cards):\n\n" +
|
|
551
|
+
blocksText;
|
|
552
|
+
const existingForMention = result.existingConnectionsForMention ?? result.existingConnections ?? [];
|
|
553
|
+
if (existingForMention.length > 0) {
|
|
554
|
+
message +=
|
|
555
|
+
"\n\nYou already have a connection with: " +
|
|
556
|
+
existingForMention.map((c) => c.name + (c.status ? " (" + c.status + ")" : "")).join(", ") +
|
|
557
|
+
". View on your home page.";
|
|
558
|
+
}
|
|
559
|
+
const totalRemaining = (result.pagination?.remaining ?? 0) + extraFromCap;
|
|
560
|
+
if (totalRemaining > 0 && result.pagination?.discoveryId) {
|
|
561
|
+
message += `\n\nThere are ${totalRemaining} more candidates. Ask if the user wants to see more — they can say "show me more" and you should call create_opportunities with continueFrom="${result.pagination.discoveryId}".`;
|
|
562
|
+
}
|
|
563
|
+
else if (isIntroducerFlow) {
|
|
564
|
+
message += `\n\nThese are all the introduction candidates I found for this person.`;
|
|
565
|
+
}
|
|
566
|
+
else {
|
|
567
|
+
message += `\n\nThese are all the connections I found. If the user wants to attract more connections, suggest they create a signal — e.g. "Would you like to create a signal so others looking for someone like you can find you?" If they agree, call create_intent with a description based on what they were searching for.`;
|
|
568
|
+
}
|
|
569
|
+
return success({
|
|
570
|
+
found: true,
|
|
571
|
+
count: displayedBlocks.length,
|
|
572
|
+
message,
|
|
573
|
+
summary: `Found ${displayedBlocks.length} match(es)`,
|
|
574
|
+
...(result.existingConnections?.length ? { existingConnections: result.existingConnections } : {}),
|
|
575
|
+
...(result.pagination ? { pagination: result.pagination } : {}),
|
|
576
|
+
debugSteps: allDebugSteps,
|
|
577
|
+
// Distinct from `createIntentSuggested` (no-results path) intentionally:
|
|
578
|
+
// `handleCreateIntentCallback` in chat.agent.ts auto-creates for that key.
|
|
579
|
+
// This flag is for the results-found path where the agent must ask the user first.
|
|
580
|
+
...(searchQuery && !query.targetUserId && !isIntroducerFlow
|
|
581
|
+
? {
|
|
582
|
+
suggestIntentCreationForVisibility: true,
|
|
583
|
+
suggestedIntentDescription: searchQuery,
|
|
584
|
+
}
|
|
585
|
+
: {}),
|
|
586
|
+
_graphTimings: _discoverGraphTimings,
|
|
587
|
+
});
|
|
588
|
+
},
|
|
589
|
+
});
|
|
590
|
+
const listOpportunities = defineTool({
|
|
591
|
+
name: "list_opportunities",
|
|
592
|
+
description: "Lists the user's opportunities (suggested connections). Returns opportunity cards to display. When chat is index-scoped, only shows opportunities from that index.",
|
|
593
|
+
querySchema: z.object({
|
|
594
|
+
indexId: z
|
|
595
|
+
.string()
|
|
596
|
+
.optional()
|
|
597
|
+
.describe("Index UUID filter; defaults to current index when scoped."),
|
|
598
|
+
}),
|
|
599
|
+
handler: async ({ context, query }) => {
|
|
600
|
+
// Strict scope enforcement: when chat is index-scoped, only allow that index
|
|
601
|
+
if (context.indexId &&
|
|
602
|
+
query.indexId?.trim() &&
|
|
603
|
+
query.indexId.trim() !== context.indexId) {
|
|
604
|
+
return error("This chat is scoped to " +
|
|
605
|
+
(context.indexName ?? "this index") +
|
|
606
|
+
". You can only list opportunities from this community.");
|
|
607
|
+
}
|
|
608
|
+
const effectiveIndexId = (context.indexId || query.indexId?.trim()) ?? undefined;
|
|
609
|
+
if (effectiveIndexId && !UUID_REGEX.test(effectiveIndexId)) {
|
|
610
|
+
return error("Invalid index ID format.");
|
|
611
|
+
}
|
|
612
|
+
// Get opportunities; use minimal card data (no LLM presenter) for fast chat response
|
|
613
|
+
const opportunities = await database.getOpportunitiesForUser(context.userId, {
|
|
614
|
+
indexId: effectiveIndexId,
|
|
615
|
+
limit: CHAT_DISPLAY_LIMIT,
|
|
616
|
+
});
|
|
617
|
+
if (!opportunities || opportunities.length === 0) {
|
|
618
|
+
return success({
|
|
619
|
+
found: false,
|
|
620
|
+
count: 0,
|
|
621
|
+
summary: "No opportunities yet",
|
|
622
|
+
message: "You have no opportunities yet. Use create_opportunities to find connections.",
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
// Batch-fetch profiles and users for all counterpart and introducer userIds to avoid N+1
|
|
626
|
+
const counterpartUserIds = new Set();
|
|
627
|
+
const introducerUserIds = new Set();
|
|
628
|
+
for (const opp of opportunities) {
|
|
629
|
+
const counterpartActor = opp.actors.find((a) => a.userId !== context.userId && a.role !== "introducer");
|
|
630
|
+
if (counterpartActor?.userId)
|
|
631
|
+
counterpartUserIds.add(counterpartActor.userId);
|
|
632
|
+
const introducerActor = opp.actors.find((a) => a.role === "introducer" && a.userId !== context.userId);
|
|
633
|
+
if (introducerActor?.userId)
|
|
634
|
+
introducerUserIds.add(introducerActor.userId);
|
|
635
|
+
}
|
|
636
|
+
const allUserIds = [
|
|
637
|
+
...new Set([...counterpartUserIds, ...introducerUserIds]),
|
|
638
|
+
];
|
|
639
|
+
const [viewerUser, profileResults, userResults] = await Promise.all([
|
|
640
|
+
database.getUser(context.userId),
|
|
641
|
+
Promise.all(allUserIds.map((id) => database.getProfile(id))),
|
|
642
|
+
Promise.all(allUserIds.map((id) => database.getUser(id))),
|
|
643
|
+
]);
|
|
644
|
+
const viewerName = viewerUser?.name ?? undefined;
|
|
645
|
+
const profileMap = new Map();
|
|
646
|
+
const userMap = new Map();
|
|
647
|
+
allUserIds.forEach((userId, i) => {
|
|
648
|
+
const profile = profileResults[i] ?? null;
|
|
649
|
+
const user = userResults[i] ?? null;
|
|
650
|
+
if (profile)
|
|
651
|
+
profileMap.set(userId, profile);
|
|
652
|
+
if (user)
|
|
653
|
+
userMap.set(userId, user);
|
|
654
|
+
});
|
|
655
|
+
const opportunityBlocks = [];
|
|
656
|
+
const seenOpportunityIds = new Set();
|
|
657
|
+
const skippedCards = [];
|
|
658
|
+
for (const opp of opportunities) {
|
|
659
|
+
if (seenOpportunityIds.has(opp.id))
|
|
660
|
+
continue;
|
|
661
|
+
seenOpportunityIds.add(opp.id);
|
|
662
|
+
try {
|
|
663
|
+
const counterpartActor = opp.actors.find((a) => a.userId !== context.userId && a.role !== "introducer");
|
|
664
|
+
const counterpartUserId = counterpartActor?.userId;
|
|
665
|
+
if (!counterpartUserId)
|
|
666
|
+
continue;
|
|
667
|
+
const viewerIsIntroducerHere = opp.actors.some((a) => a.role === "introducer" && a.userId === context.userId);
|
|
668
|
+
const secondPartyActorForHeadline = viewerIsIntroducerHere
|
|
669
|
+
? opp.actors.find((a) => a.userId !== context.userId &&
|
|
670
|
+
a.userId !== counterpartUserId &&
|
|
671
|
+
a.role !== "introducer")
|
|
672
|
+
: undefined;
|
|
673
|
+
const secondPartyNameForHeadline = secondPartyActorForHeadline
|
|
674
|
+
? (profileMap.get(secondPartyActorForHeadline.userId)?.identity?.name ??
|
|
675
|
+
userMap.get(secondPartyActorForHeadline.userId)?.name ??
|
|
676
|
+
undefined)
|
|
677
|
+
: undefined;
|
|
678
|
+
const introducerActor = opp.actors.find((a) => a.role === "introducer" && a.userId !== context.userId);
|
|
679
|
+
const createdByName = opp.detection.createdByName;
|
|
680
|
+
const counterpartProfile = profileMap.get(counterpartUserId) ?? null;
|
|
681
|
+
const counterpartUser = userMap.get(counterpartUserId) ?? null;
|
|
682
|
+
const introducerProfile = introducerActor && !createdByName
|
|
683
|
+
? profileMap.get(introducerActor.userId) ?? null
|
|
684
|
+
: null;
|
|
685
|
+
const counterpartName = counterpartProfile?.identity?.name ??
|
|
686
|
+
counterpartUser?.name ??
|
|
687
|
+
"Someone";
|
|
688
|
+
const introducerName = createdByName ??
|
|
689
|
+
(introducerActor ? introducerProfile?.identity?.name ?? null : null);
|
|
690
|
+
const introducerUser = introducerActor
|
|
691
|
+
? userMap.get(introducerActor.userId) ?? null
|
|
692
|
+
: null;
|
|
693
|
+
const secondPartyUser = secondPartyActorForHeadline
|
|
694
|
+
? userMap.get(secondPartyActorForHeadline.userId) ?? null
|
|
695
|
+
: null;
|
|
696
|
+
const cardData = buildMinimalOpportunityCard(opp, context.userId, counterpartUserId, counterpartName, counterpartUser?.avatar ?? null, introducerName, introducerUser?.avatar ?? null, viewerName, secondPartyNameForHeadline, secondPartyUser?.avatar ?? null, secondPartyActorForHeadline?.userId);
|
|
697
|
+
opportunityBlocks.push(CODE_FENCE + "opportunity\n" +
|
|
698
|
+
sanitizeJsonForCodeFence(JSON.stringify(cardData)) +
|
|
699
|
+
"\n" + CODE_FENCE);
|
|
700
|
+
}
|
|
701
|
+
catch (err) {
|
|
702
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
703
|
+
logger.warn("Skipping opportunity that failed to build minimal card", {
|
|
704
|
+
opportunityId: opp.id,
|
|
705
|
+
error: errMsg,
|
|
706
|
+
});
|
|
707
|
+
skippedCards.push({ opportunityId: opp.id, error: errMsg });
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
const listDebugSteps = [];
|
|
712
|
+
if (skippedCards.length > 0) {
|
|
713
|
+
listDebugSteps.push({
|
|
714
|
+
step: "card_build_errors",
|
|
715
|
+
detail: `${skippedCards.length} opportunity card(s) failed to build`,
|
|
716
|
+
data: {
|
|
717
|
+
skippedCount: skippedCards.length,
|
|
718
|
+
totalOpportunities: opportunities.length,
|
|
719
|
+
errors: skippedCards,
|
|
720
|
+
},
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
if (opportunityBlocks.length === 0) {
|
|
724
|
+
if (skippedCards.length > 0) {
|
|
725
|
+
return success({
|
|
726
|
+
found: false,
|
|
727
|
+
count: 0,
|
|
728
|
+
summary: "Some opportunities couldn't be displayed",
|
|
729
|
+
message: "I found opportunities, but couldn't render them. Please try again.",
|
|
730
|
+
...(listDebugSteps.length ? { debugSteps: listDebugSteps } : {}),
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
return success({
|
|
734
|
+
found: false,
|
|
735
|
+
count: 0,
|
|
736
|
+
summary: "No opportunities yet",
|
|
737
|
+
message: "You have no opportunities yet. Use create_opportunities to find connections.",
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
// Join all opportunity blocks into a single string for the LLM to include verbatim
|
|
741
|
+
const blocksText = opportunityBlocks.join("\n\n");
|
|
742
|
+
return success({
|
|
743
|
+
found: true,
|
|
744
|
+
count: opportunityBlocks.length,
|
|
745
|
+
summary: `You have ${opportunityBlocks.length} opportunity(ies)`,
|
|
746
|
+
message: "You have " +
|
|
747
|
+
opportunityBlocks.length +
|
|
748
|
+
" opportunity(ies). IMPORTANT: Include the following " +
|
|
749
|
+
CODE_FENCE +
|
|
750
|
+
"opportunity code blocks EXACTLY as-is in your response (they render as interactive cards):\n\n" +
|
|
751
|
+
blocksText,
|
|
752
|
+
...(listDebugSteps.length ? { debugSteps: listDebugSteps } : {}),
|
|
753
|
+
});
|
|
754
|
+
},
|
|
755
|
+
});
|
|
756
|
+
const updateOpportunity = defineTool({
|
|
757
|
+
name: "update_opportunity",
|
|
758
|
+
description: "Updates an opportunity's status. Use 'pending' to send a draft (notifies next person). Use 'accepted'/'rejected' to respond to a received opportunity. When chat is index-scoped, can only update opportunities from that index.",
|
|
759
|
+
querySchema: z.object({
|
|
760
|
+
opportunityId: z
|
|
761
|
+
.string()
|
|
762
|
+
.describe("Opportunity ID from list_opportunities"),
|
|
763
|
+
status: z
|
|
764
|
+
.enum(["pending", "accepted", "rejected", "expired"])
|
|
765
|
+
.describe("New status: pending (send draft), accepted, rejected, expired"),
|
|
766
|
+
}),
|
|
767
|
+
handler: async ({ context, query }) => {
|
|
768
|
+
const opportunityId = query.opportunityId?.trim();
|
|
769
|
+
if (!opportunityId || !UUID_REGEX.test(opportunityId)) {
|
|
770
|
+
return error("Valid opportunityId required.");
|
|
771
|
+
}
|
|
772
|
+
// Strict scope enforcement: when chat is index-scoped, verify opportunity is in that index
|
|
773
|
+
if (context.indexId) {
|
|
774
|
+
const opportunity = await systemDb.getOpportunity(opportunityId);
|
|
775
|
+
if (!opportunity) {
|
|
776
|
+
return error("Opportunity not found.");
|
|
777
|
+
}
|
|
778
|
+
const opportunityIndexId = opportunity.context?.indexId;
|
|
779
|
+
if (!opportunityIndexId || opportunityIndexId !== context.indexId) {
|
|
780
|
+
return error("Opportunity not found.");
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
const isSend = query.status === "pending";
|
|
784
|
+
const _updateGraphStart = Date.now();
|
|
785
|
+
const _updateTraceEmitter = requestContext.getStore()?.traceEmitter;
|
|
786
|
+
_updateTraceEmitter?.({ type: "graph_start", name: "opportunity" });
|
|
787
|
+
const result = await graphs.opportunity.invoke({
|
|
788
|
+
userId: context.userId,
|
|
789
|
+
operationMode: isSend ? "send" : "update",
|
|
790
|
+
opportunityId: query.opportunityId,
|
|
791
|
+
...(isSend ? {} : { newStatus: query.status }),
|
|
792
|
+
});
|
|
793
|
+
const _updateGraphMs = Date.now() - _updateGraphStart;
|
|
794
|
+
_updateTraceEmitter?.({ type: "graph_end", name: "opportunity", durationMs: _updateGraphMs });
|
|
795
|
+
if (result.mutationResult) {
|
|
796
|
+
if (result.mutationResult.success) {
|
|
797
|
+
return success({
|
|
798
|
+
opportunityId: result.mutationResult.opportunityId,
|
|
799
|
+
status: query.status,
|
|
800
|
+
message: result.mutationResult.message,
|
|
801
|
+
...(result.mutationResult.notified && {
|
|
802
|
+
notified: result.mutationResult.notified,
|
|
803
|
+
}),
|
|
804
|
+
_graphTimings: [{ name: 'opportunity', durationMs: _updateGraphMs, agents: result.agentTimings ?? [] }],
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
return error(result.mutationResult.error || "Failed to update opportunity.");
|
|
808
|
+
}
|
|
809
|
+
return error("Failed to update opportunity.");
|
|
810
|
+
},
|
|
811
|
+
});
|
|
812
|
+
return [createOpportunities, listOpportunities, updateOpportunity];
|
|
813
|
+
}
|
|
814
|
+
//# sourceMappingURL=opportunity.tools.js.map
|