Purpose: AI-powered path suggestion API that helps AI clients (e.g., Claude via MCP) determine optimal save locations for page content in GROWI. The system analyzes content, searches for related pages, evaluates candidates, and returns directory path suggestions with metadata.
Users: AI clients (Claude via MCP) call this endpoint on behalf of GROWI users during the "save to GROWI" workflow.
ai-tools namespace from /pageinformationType, type, grant) alongside natural language (description) so that even less capable LLM clients can make correct decisions.POST /_api/v3/page handles this)graph TB
subgraph Client
MCP[MCP Server]
end
subgraph GROWI_API[GROWI API]
Router[ai-tools Router]
Handler[suggest-path Handler]
MemoGen[Memo Suggestion]
Analyzer[Content Analyzer - 1st AI Call]
Retriever[Search Candidate Retriever]
Evaluator[Candidate Evaluator - 2nd AI Call]
CategoryGen[Category Suggestion - Under Review]
end
subgraph Existing[Existing Services]
SearchSvc[Search Service]
GrantSvc[Page Grant Service]
AIFeature[GROWI AI - OpenAI Feature]
end
subgraph Data
ES[Elasticsearch]
Mongo[MongoDB - Pages]
end
MCP -->|POST suggest-path| Router
Router --> Handler
Handler --> MemoGen
Handler --> Analyzer
Analyzer --> AIFeature
Handler --> Retriever
Retriever --> SearchSvc
Handler --> Evaluator
Evaluator --> AIFeature
Handler --> CategoryGen
CategoryGen --> SearchSvc
SearchSvc --> ES
Evaluator --> GrantSvc
CategoryGen --> GrantSvc
GrantSvc --> Mongo
Integration notes:
res.apiv3() response formatAll suggest-path code resides in features/ai-tools/suggest-path/ following the project's feature-based architecture pattern.
apps/app/src/features/ai-tools/
├── server/routes/apiv3/
│ └── index.ts # Aggregation router for ai-tools namespace
└── suggest-path/
├── interfaces/
│ └── suggest-path-types.ts # Shared types (PathSuggestion, ContentAnalysis, etc.)
└── server/
├── routes/apiv3/
│ ├── index.ts # Route factory, handler + middleware chain
│ └── index.spec.ts
├── services/
│ ├── generate-suggestions.ts # Orchestrator
│ ├── generate-memo-suggestion.ts
│ ├── analyze-content.ts # AI call #1: keyword extraction + flow/stock
│ ├── retrieve-search-candidates.ts # ES search with score filtering
│ ├── evaluate-candidates.ts # AI call #2: candidate evaluation + path proposal
│ ├── call-llm-for-json.ts # Shared LLM call utility
│ ├── generate-category-suggestion.ts # Under review
│ ├── resolve-parent-grant.ts
│ └── *.spec.ts # Co-located tests
└── integration-tests/
└── suggest-path-integration.spec.ts
Key decisions:
features/openai/ convention)ai-tools router at features/ai-tools/server/routes/apiv3/ imports the suggest-path route factory. This allows future ai-tools features to register under the same namespaceAll components are pure functions with immutable data. No classes — no component currently meets class adoption criteria (shared dependency management or singleton state).
sequenceDiagram
participant Client as MCP Client
participant Handler as Orchestrator
participant AI1 as Content Analyzer
participant Search as Search Service
participant AI2 as Candidate Evaluator
participant Grant as Grant Resolver
participant CatGen as Category Generator
Client->>Handler: POST with body content
Handler->>Handler: Generate memo suggestion
Handler->>AI1: Analyze content body
Note over AI1: 1st AI Call
AI1-->>Handler: keywords + informationType
par Search and evaluate
Handler->>Search: Search by keywords
Search-->>Handler: Raw results with scores
Handler->>Handler: Filter by score threshold
Handler->>AI2: body + analysis + candidates
Note over AI2: 2nd AI Call
AI2-->>Handler: Evaluated suggestions with paths and descriptions
loop For each evaluated suggestion
Handler->>Grant: Resolve grant for proposed path
Grant-->>Handler: Grant value
end
and Category suggestion
Handler->>CatGen: Generate from keywords
CatGen->>Search: Scoped keyword search
Search-->>CatGen: Top-level pages
CatGen->>Grant: Resolve parent grant
Grant-->>CatGen: Grant value
CatGen-->>Handler: Category suggestion or null
end
Handler-->>Client: 200 suggestions array
Key decisions:
function generateSuggestions(
user: IUserHasId,
body: string,
userGroups: ObjectIdLike[],
searchService: SearchService,
): Promise<PathSuggestion[]>;
searchService is passed as a parameter (the sole external dependency that cannot be statically imported)ContentAnalysis.informationType to each search-type suggestion (Req 13.1)type ContentAnalysis = {
keywords: string[]; // 1-5 keywords, proper nouns prioritized
informationType: 'flow' | 'stock';
};
function analyzeContent(body: string): Promise<ContentAnalysis>;
type SearchCandidate = {
pagePath: string;
snippet: string;
score: number;
};
function retrieveSearchCandidates(
keywords: string[],
user: IUserHasId,
userGroups: ObjectIdLike[],
searchService: SearchService,
): Promise<SearchCandidate[]>;
searchService is a direct positional argument (not wrapped in an options object)SCORE_THRESHOLD = 5.0)type EvaluatedSuggestion = {
path: string; // Proposed directory path with trailing /
label: string;
description: string; // AI-generated rationale
};
function evaluateCandidates(
body: string,
analysis: ContentAnalysis,
candidates: SearchCandidate[],
): Promise<EvaluatedSuggestion[]>;
function generateCategorySuggestion(
candidates: SearchCandidate[],
): Promise<PathSuggestion | null>;
null when no matching top-level pages are foundfunction resolveParentGrant(dirPath: string): Promise<number>;
GRANT_OWNER (4) as safe default if no ancestor found| Method | Endpoint | Request | Response | Errors |
|---|---|---|---|---|
| POST | /_api/v3/ai-tools/suggest-path |
SuggestPathRequest |
SuggestPathResponse |
400, 401, 403, 500 |
// Request
interface SuggestPathRequest {
body: string; // Page content for analysis (required, non-empty)
}
// Response
type SuggestionType = 'memo' | 'search' | 'category';
type InformationType = 'flow' | 'stock';
interface PathSuggestion {
type: SuggestionType;
path: string; // Directory path with trailing '/'
label: string;
description: string; // Fixed for memo, AI-generated for search
grant: number; // Parent page grant (PageGrant value)
informationType?: InformationType; // Search-based only
}
interface SuggestPathResponse {
suggestions: PathSuggestion[]; // Always ≥1 element (memo)
}
Invariants: path ends with /, grant is a valid PageGrant value (1, 2, 4, or 5)
{
"suggestions": [
{
"type": "memo",
"path": "/user/alice/memo/",
"label": "Save as memo",
"description": "Save to your personal memo area",
"grant": 4
},
{
"type": "search",
"path": "/tech-notes/React/state-management/",
"label": "Save near related pages",
"description": "This area contains pages about React state management. Your stock content fits well alongside this existing reference material.",
"grant": 1,
"informationType": "stock"
},
{
"type": "category",
"path": "/tech-notes/",
"label": "Save under category",
"description": "Top-level category: tech-notes",
"grant": 1
}
]
}
| Error | Status | Requirement |
|---|---|---|
Missing or empty body |
400 | 9.1 |
| No authentication | 401 | 8.2 |
| AI service not enabled | 403 | 1.4 |
| Failure | Fallback |
|---|---|
| Content analysis (1st AI call) | Memo only (skips entire search pipeline) |
| Search service | Memo + category (if available) |
| Candidate evaluation (2nd AI call) | Memo + category (if available) |
| Category generation | Memo + search-based (if available) |
Each component fails independently. Memo is always generated first as guaranteed fallback.
searchKeyword() user/group parameters