From dd36afd49cf3f3cdd725d06f869ca094ce6a775e Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 20 Sep 2025 16:36:35 +0000 Subject: [PATCH] Revert "Fix chat prototype build (#287)" This reverts commit 96d0124d5b5669096b96b02d1c3f93654797ba9d. --- .cursor/rules/overview.mdc | 6 +- .cursor/rules/tutorials-structure.mdc | 69 - .env.example | 5 +- .gitignore | 1 - agent-docs/RAG-TODO.md | 54 + agent-docs/RAG-design.md | 220 + agent-docs/RAG-user-stories.md | 116 + agent-docs/README.md | 6 +- agent-docs/agentuity.yaml | 3 - agent-docs/bun.lock | 20 +- agent-docs/package.json | 2 +- agent-docs/src/agents/agent-pulse/README.md | 102 - .../src/agents/agent-pulse/context/builder.ts | 54 - agent-docs/src/agents/agent-pulse/index.ts | 143 - .../src/agents/agent-pulse/request/parser.ts | 49 - .../src/agents/agent-pulse/request/types.ts | 16 - agent-docs/src/agents/agent-pulse/state.ts | 46 - .../src/agents/agent-pulse/state/manager.ts | 54 - .../agents/agent-pulse/streaming/processor.ts | 115 - .../src/agents/agent-pulse/streaming/types.ts | 48 - agent-docs/src/agents/agent-pulse/tools.ts | 103 - agent-docs/src/agents/agent-pulse/tutorial.ts | 98 - agent-docs/src/agents/doc-qa/prompt.ts | 1 + agent-docs/src/agents/doc-qa/rag.ts | 7 +- app/(docs)/[[...slug]]/page.tsx | 2 - app/api/rag-search/route.ts | 4 +- .../sessions/[sessionId]/messages/route.ts | 374 - app/api/sessions/[sessionId]/route.ts | 266 - app/api/sessions/route.ts | 149 - app/api/tutorials/[id]/route.ts | 43 - .../[id]/steps/[stepNumber]/route.ts | 178 - app/api/tutorials/route.ts | 88 - app/api/users/tutorial-state/route.ts | 119 - app/chat/SessionContext.tsx | 21 - app/chat/[sessionId]/page.tsx | 233 - app/chat/components/ChatInput.tsx | 74 - app/chat/components/ChatMessage.tsx | 155 - app/chat/components/ChatMessagesArea.tsx | 70 - app/chat/components/CodeBlock.tsx | 62 - app/chat/components/CodeEditor.tsx | 135 - app/chat/components/MarkdownRenderer.tsx | 85 - app/chat/components/SessionSidebar.tsx | 205 - .../components/SessionSidebarSkeleton.tsx | 100 - app/chat/components/TutorialFileChip.tsx | 33 - app/chat/layout.tsx | 114 - app/chat/page.tsx | 57 - app/chat/services/sessionService.ts | 296 - app/chat/types.ts | 101 - app/chat/utils/dateUtils.ts | 37 - app/chat/utils/useAutoResize.ts | 45 - app/chat/utils/useStreaming.ts | 209 - app/global.css | 61 - components/CodeFromFiles.tsx | 92 - components/ui/skeleton.tsx | 16 - lib/config.ts | 12 - lib/env.ts | 94 +- lib/kv-store.ts | 288 - lib/tutorial/all-tutorials-reader.ts | 77 - lib/tutorial/index.ts | 3 - lib/tutorial/state-manager.ts | 121 - lib/tutorial/tutorial-reader.ts | 144 - lib/tutorial/types.ts | 23 - lib/validation/middleware.ts | 141 - middleware.ts | 22 +- package-lock.json | 8499 ++++++++++------- package.json | 21 +- 66 files changed, 5339 insertions(+), 8868 deletions(-) delete mode 100644 .cursor/rules/tutorials-structure.mdc create mode 100644 agent-docs/RAG-TODO.md create mode 100644 agent-docs/RAG-design.md create mode 100644 agent-docs/RAG-user-stories.md delete mode 100644 agent-docs/src/agents/agent-pulse/README.md delete mode 100644 agent-docs/src/agents/agent-pulse/context/builder.ts delete mode 100644 agent-docs/src/agents/agent-pulse/index.ts delete mode 100644 agent-docs/src/agents/agent-pulse/request/parser.ts delete mode 100644 agent-docs/src/agents/agent-pulse/request/types.ts delete mode 100644 agent-docs/src/agents/agent-pulse/state.ts delete mode 100644 agent-docs/src/agents/agent-pulse/state/manager.ts delete mode 100644 agent-docs/src/agents/agent-pulse/streaming/processor.ts delete mode 100644 agent-docs/src/agents/agent-pulse/streaming/types.ts delete mode 100644 agent-docs/src/agents/agent-pulse/tools.ts delete mode 100644 agent-docs/src/agents/agent-pulse/tutorial.ts delete mode 100644 app/api/sessions/[sessionId]/messages/route.ts delete mode 100644 app/api/sessions/[sessionId]/route.ts delete mode 100644 app/api/sessions/route.ts delete mode 100644 app/api/tutorials/[id]/route.ts delete mode 100644 app/api/tutorials/[id]/steps/[stepNumber]/route.ts delete mode 100644 app/api/tutorials/route.ts delete mode 100644 app/api/users/tutorial-state/route.ts delete mode 100644 app/chat/SessionContext.tsx delete mode 100644 app/chat/[sessionId]/page.tsx delete mode 100644 app/chat/components/ChatInput.tsx delete mode 100644 app/chat/components/ChatMessage.tsx delete mode 100644 app/chat/components/ChatMessagesArea.tsx delete mode 100644 app/chat/components/CodeBlock.tsx delete mode 100644 app/chat/components/CodeEditor.tsx delete mode 100644 app/chat/components/MarkdownRenderer.tsx delete mode 100644 app/chat/components/SessionSidebar.tsx delete mode 100644 app/chat/components/SessionSidebarSkeleton.tsx delete mode 100644 app/chat/components/TutorialFileChip.tsx delete mode 100644 app/chat/layout.tsx delete mode 100644 app/chat/page.tsx delete mode 100644 app/chat/services/sessionService.ts delete mode 100644 app/chat/types.ts delete mode 100644 app/chat/utils/dateUtils.ts delete mode 100644 app/chat/utils/useAutoResize.ts delete mode 100644 app/chat/utils/useStreaming.ts delete mode 100644 components/CodeFromFiles.tsx delete mode 100644 components/ui/skeleton.tsx delete mode 100644 lib/config.ts delete mode 100644 lib/kv-store.ts delete mode 100644 lib/tutorial/all-tutorials-reader.ts delete mode 100644 lib/tutorial/index.ts delete mode 100644 lib/tutorial/state-manager.ts delete mode 100644 lib/tutorial/tutorial-reader.ts delete mode 100644 lib/tutorial/types.ts delete mode 100644 lib/validation/middleware.ts diff --git a/.cursor/rules/overview.mdc b/.cursor/rules/overview.mdc index 4a8925ac..51937d1b 100644 --- a/.cursor/rules/overview.mdc +++ b/.cursor/rules/overview.mdc @@ -10,8 +10,4 @@ This is the technical documentation for the Agentuity Cloud, which covers: - Examples, tutorials, samples (/content/Examples) - And the different SDKs (/content/SDKs) -This doc app is a NextJS app built on top of Fumadocs (https://fumadocs.vercel.app/docs/ui). - -This project also contains the agent the powers RAG and Tutorials. Those agents live in `/agent-docs` directory. -To run the agent server locally, go into that directory with `cd agent-docs` and then start the agent with `agentuity dev`. -To run the NextJS app, do `npm run dev` in the root of this repository. You will want to start these two apps in separate repositories. \ No newline at end of file +This doc app is a NextJS app built on top of Fumadocs (https://fumadocs.vercel.app/docs/ui). \ No newline at end of file diff --git a/.cursor/rules/tutorials-structure.mdc b/.cursor/rules/tutorials-structure.mdc deleted file mode 100644 index bf534346..00000000 --- a/.cursor/rules/tutorials-structure.mdc +++ /dev/null @@ -1,69 +0,0 @@ -### Rule: tutorials-structure - -Purpose: Define how tutorials are authored and rendered so docs and runnable code stay in sync. - -### Structure -- Narrative pages: `content/Tutorial//step-.mdx` -- Section navigation: `content/Tutorial/meta.json` (list MDX page slugs in order) -- Runnable example project: `examples//` (full project; exclude `node_modules/`, `.uv/`) -- Tutorial code snippets: must be imported from files via `` -- Other, non-project examples: use regular fenced code blocks in MDX - -### MDX authoring -- Each step MDX must use frontmatter: - - `title`, `description` (recommended: `stepNumber`, `next`, `prev`) -- Import real code from the example project using repo-root-relative paths via ``: - -```mdx ---- -title: Step 1 — Getting Started -description: Intro step ---- - - - - - - - - - -``` - -### `` component -- Available in MDX via the docs page components map -- Props: - - `snippets`: array of `{ path, lang?, from?, to?, title? }` - - `path`: repo-root-relative (must start with `/`), validated and read server-side - - `lang`: language for highlighting (auto-inferred if omitted) - - `from`, `to`: 1-based line range (inclusive) - - `title`: label per tab - - `title` (optional): heading displayed above the tabs -- Renders using shared `CodeBlock` styling and `Tabs` for multiple snippets - -### Example project conventions (`examples//`) -- Include: `package.json`, `tsconfig.json`, `src/**`, optional `README.md`, optional lockfile -- Exclude: `node_modules/` -- Optional hygiene: - - Add `examples/**` to `.eslintignore` if you don’t want repo-wide linting on example sources - - Add `examples/**` to root `tsconfig.json` `exclude` if you don’t want repo-wide type-checks -- Should be runnable in a sandbox (StackBlitz/CodeSandbox) via `package.json` scripts - -### Rendering pipeline -- Fumadocs loads MDX via `lib/source.ts` and `app/(docs)/[[...slug]]/page.tsx` -- `` reads files at build/server time and renders with `CodeBlock` - - -### Agent compatibility (optional) -- Step API: `GET /api/tutorials/:id/steps/:stepNumber` returns `{ mdx, snippets }`. -- The chat UI replaces each `` occurrence with one or more fenced code blocks by consuming entries from `snippets` in order. - -### Quality gates (recommended) -- CI verifies: - - All `content/Tutorial/**.mdx` pages referenced by `content/Tutorial/meta.json` build without errors - - All `` references resolve to existing files - - Optional: the example project type-checks (`tsc --noEmit`) or passes a smoke test - diff --git a/.env.example b/.env.example index 1f4d2a79..9f33510c 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,6 @@ # Agent Configuration AGENT_BASE_URL=http://127.0.0.1:3500 +AGENT_ID=agent_9ccc5545e93644bd9d7954e632a55a61 -# API key can be found in agent-docs .env AGENTUITY_SDK_KEY -AGENTUITY_API_KEY= \ No newline at end of file +# Alternative: You can also set the full URL instead of BASE_URL + ID +# AGENT_FULL_URL=http://127.0.0.1:3500/agent_9ccc5545e93644bd9d7954e632a55a61 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 6c466111..38bf69a4 100644 --- a/.gitignore +++ b/.gitignore @@ -26,7 +26,6 @@ yarn-error.log* .env*.local .env.local .env.production -.env .vercel next-env.d.ts .open-next diff --git a/agent-docs/RAG-TODO.md b/agent-docs/RAG-TODO.md new file mode 100644 index 00000000..beb72cf3 --- /dev/null +++ b/agent-docs/RAG-TODO.md @@ -0,0 +1,54 @@ +# RAG System Implementation TODOs + +## 1. Document Chunking & Metadata +- [x] Refine and test the chunking logic for MDX files. +- [x] Implement full metadata enrichment (id, path, chunkIndex, contentType, heading, keywords) in the chunking/processing pipeline. +- [x] Write unit tests for chunking and metadata extraction. + +## 2. Keyword Extraction +- [x] Implement LLM-based keyword extraction for each chunk. +- [x] Write tests to validate keyword extraction quality. +- [ ] Integrate keyword in document processing pipeline + +## 3. Embedding Generation +- [x] Implement embedding function for batch processing of chunk texts (using OpenAI SDK or Agentuity vector store as appropriate). +- [x] Integrate embedding generation into the chunk processing pipeline. +- [ ] Write tests to ensure embeddings are generated and stored correctly. + +## 4. Vector Store Integration +- [x] Set up Agentuity vector database integration. +- [x] Store chunk content, metadata, keywords, and embeddings. + +## 5. Hybrid Retrieval Logic +- [ ] Implement hybrid search (semantic + keyword boosting). +- [ ] Write tests to ensure correct ranking and recall. + +## 6. Reranker Integration +- [ ] Integrate reranker model (API or local). +- [ ] Implement reranking step after hybrid retrieval. +- [ ] Write tests to validate reranker improves result quality. + +## 7. API Layer +- [ ] Build modular API endpoints for search and retrieval. +- [ ] Ensure endpoints are stateless and testable. +- [ ] Write API tests (unit and integration). + +## 8. UI Integration +- [ ] Add search bar and results display to documentation site. +- [ ] Implement keyword highlighting and breadcrumb navigation. +- [ ] Write UI tests for search and result presentation. + +## 9. Monitoring & Analytics +- [ ] Add logging for search queries and result quality. +- [ ] Implement feedback mechanism for users to rate results. + +## 10. Documentation & Developer Experience +- [ ] Document each module and its tests. +- [ ] Provide clear setup and usage instructions. + +## 11. Sync/Processor Workflow Design +- [x] Design the documentation sync workflow: + - [x] Primary: Trigger sync via CI/CD or GitHub Action after merges to main/deploy branch. + - [x] Optional: Implement a webhook endpoint for manual or CMS-triggered syncs. + - [x] Ensure the sync process is idempotent and efficient (only updates changed docs/chunks). + - [x] Plan for operational workflow implementation after core modules are complete. diff --git a/agent-docs/RAG-design.md b/agent-docs/RAG-design.md new file mode 100644 index 00000000..7e350054 --- /dev/null +++ b/agent-docs/RAG-design.md @@ -0,0 +1,220 @@ +# Documentation RAG System - Design Document + +## 1. System Overview +A Retrieval-Augmented Generation (RAG) system for Agentuity's documentation, enabling users to search for relevant documentation pages, get direct answers, and discover code examples efficiently. + +--- + +## 2. Document Chunking & Metadata + +### 2.1 Chunking +- Documents (MDX files) are split into semantically meaningful chunks (steps, paragraphs, code blocks, etc.) using custom logic. +- Each chunk is enriched with metadata for effective retrieval and navigation. + +### 2.2 Metadata Structure +```typescript +interface DocumentMetadata { + id: string; + path: string; + chunkIndex: number; + contentType: string; + heading?: string; + keywords?: string; +} +``` + +#### Field Rationale & Use Cases +- **id**: Unique retrieval, deduplication, updates. +- **path**: Navigation, linking, analytics. +- **chunkIndex**: Context window, document flow. +- **contentType**: Result presentation, filtering. +- **keywords**: Hybrid search, filtering, boosting, related content, highlighting. + +--- + +## 3. [Optional] Keyword Extraction +- **Purpose:** Boost search accuracy, enable hybrid search, support filtering, and improve UI. +- **Approach:** + - Start with simple extraction (headings, code, links, bolded text). + - For best results, use an LLM (e.g., GPT-4o) to extract 5-10 important keywords/phrases per chunk. + - Store keywords as a separate field in the metadata. + +--- + +## 4. Embedding Generation +- **Only the main content of each chunk is embedded** (not keywords or metadata). +- Use a dedicated embedding model (e.g., OpenAI's `text-embedding-3-small`). +- Store the resulting vector alongside the chunk's metadata and keywords. + +--- + +## 5. Vector Store +- Use Agentuity built in Vector storage +- Store for each chunk: + - Embedding vector + - Main content + - Metadata (id, path, chunkIndex, contentType, heading) + - Keywords + +--- + +## 6. Retrieval & Hybrid Search +- **User query flow:** + 1. Embed the user query. + 2. Search for similar vectors (semantic search). + 3. Check for keyword matches in the `keywords` field. + 4. Combine results (hybrid search), boosting those with both high semantic similarity and keyword matches. + 5. Use metadata for context and navigation in the UI. + +- **Why not embed keywords/metadata?** + - Embedding only the main content ensures high-quality semantic search. + - Keywords/metadata are used for filtering, boosting, and UI, not for semantic similarity. + +--- + +## 7. [Optional] Keyword Boosting and Highlighting + +### 7.1 Keyword Boosting in Retrieval + +**Definition:** Boosting means giving extra weight to chunks that contain keywords matching the user's query, so they appear higher in the search results—even if their semantic similarity score is not the highest. + +**How It Works:** +- When a user submits a query: + 1. **Semantic Search:** Embed the query and retrieve the top-N most similar chunks from the vector store. + 2. **Keyword Match:** Check which of these chunks have keywords that match (exactly or fuzzily) terms in the user's query. + 3. **Score Adjustment:** Increase the score (or ranking) of chunks with keyword matches. Optionally, also include chunks that have strong keyword matches but were not in the top-N semantic results. + 4. **Hybrid Ranking:** Combine the semantic similarity score and the keyword match score to produce a final ranking. + +**Technical Example:** +- For each chunk, compute: + `final_score = semantic_score + (keyword_match ? boost_value : 0)` +- Tune `boost_value` based on how much you want to favor keyword matches. + +**Why?** +- Ensures that highly relevant technical results (e.g., containing exact function names, CLI commands, or jargon) are not missed by the embedding model. +- Improves recall for precise, technical queries. + +--- + +### 7.2 Keyword Highlighting in the UI + +**Definition:** Highlighting means visually emphasizing the keywords in the search results that match the user's query, making it easier for users to spot why a result is relevant. + +**How It Works:** +- When displaying a result chunk: + 1. Compare the user's query terms to the chunk's keywords. + 2. In the displayed snippet, bold or color the matching keywords. + 3. Optionally, also highlight those keywords in the context of the chunk's content. + +**User Experience Example:** +- User searches for: `install CLI on Linux` +- Result snippet: + ``` + The **Agentuity CLI** is a cross-platform command-line tool for working with Agentuity Cloud. It supports **Windows** (using WSL), **MacOS**, and **Linux**. + ``` +- The keywords "Agentuity CLI" and "Linux" are highlighted, helping the user quickly see the match. + +**Why?** +- Increases user trust in the search system by making relevance transparent. +- Helps users scan results faster, especially in technical documentation with dense information. + +--- + +### 7.3 Summary Table + +| Feature | Purpose | Technical Step | User Benefit | +|--------------|-----------------------------------------|---------------------------------------|-------------------------------------| +| Boosting | Improve ranking of keyword matches | Adjust score/rank in retrieval | More relevant results at the top | +| Highlighting | Make matches visible in the UI | Bold/color keywords in result display | Easier, faster result comprehension | + +--- + +### 7.4 Optional Enhancements +- Allow users to filter results by keyword/facet. +- Show a "Why this result?" tooltip listing matched keywords. + +--- + +## 8. Reranker Integration + +### 8.1 What is a Reranker? +A reranker is a model (often a cross-encoder or LLM) that takes a set of candidate results (retrieved by semantic/keyword/hybrid search) and scores them for relevance to the user's query, often with much higher accuracy than the initial retrieval. + +### 8.2 Where Does It Fit? +- The reranker is applied **after** the hybrid retrieval (semantic + keyword boosting) step. +- It takes the top-N candidate chunks and the user query, and produces a new, more accurate ranking. +- The final answer generated based on the top n context after reranked. + +### 8.3 Retrieval Pipeline with Reranker + +1. **User Query** +2. **Hybrid Retrieval** (semantic + keyword search, with boosting) +3. **Top-N Candidates** +4. **Reranker Model** (scores each candidate for true relevance) +5. **Final Generated Answer** (displayed to user) + +### 8.4 Example Models +- OpenAI GPT-4o or GPT-3.5-turbo (with a ranking prompt) +- Cohere Rerank API +- bge-reranker (open-source, HuggingFace) +- ColBERT, MonoT5, or other cross-encoders + +### 8.5 Benefits +- **Higher Precision:** Deeply understands context and technical terms. +- **Better Handling of Ambiguity:** Picks the best answer among similar candidates. +- **Improved User Trust:** More relevant answers at the top. + +### 8.6 Why Keep Keyword Search? +- Keyword search ensures exact matches for technical terms are not missed. +- Hybrid search provides the reranker with the best possible candidate set. +- Removing keyword search would reduce recall and technical accuracy. + +### 8.7 Updated Retrieval Flow Diagram + +```mermaid +graph TD + A[User Query] --> B[Hybrid Retriever (Embeddings + Keywords)] + B --> C[Top-N Candidates] + C --> D[Reranker Model] + D --> E[Final Answer] +``` + +--- + +## 9. UI Integration +- Add a search bar and results display to the documentation site. +- Show direct answers, code snippets, and links to full docs, with keyword highlighting and breadcrumb navigation. + +--- + +## 10. Technology Stack +| Step | Technology/Tool | Notes | +|---------------------|-------------------------------|----------------------------------------------------| +| Chunking | TypeScript logic | `chunk-mdx.ts` | +| Keyword Extraction | LLM (GPT-4o, GPT-3.5-turbo) | API call per chunk; can batch for efficiency | +| Embedding | OpenAI Embedding API | `text-embedding-3-small` or similar | +| Vector Store | pgvector, Pinecone, Weaviate | Choose based on infra preference | +| Retrieval API | Next.js API route | Combines vector and keyword search | +| UI | Next.js/React | Search bar, results, highlighting, navigation | + +--- + +## 11. Example Metadata for a Chunk +```json +{ + "id": "introduction-getting-started-1", + "path": "/introduction/getting-started", + "chunkIndex": 1, + "contentType": "step", + "heading": "Install the CLI", + "keywords": "Agentuity CLI, CLI installation, command-line tool, cross-platform, Windows, WSL, MacOS, Linux, curl, installation" +} +``` + +--- + +## 12. Summary +- Only main content is embedded; keywords and metadata are stored separately. +- Hybrid search (semantic + keyword) provides the best retrieval experience. +- Metadata supports navigation, filtering, and UI context. +- LLM-powered keyword extraction is recommended for technical accuracy. \ No newline at end of file diff --git a/agent-docs/RAG-user-stories.md b/agent-docs/RAG-user-stories.md new file mode 100644 index 00000000..237eb632 --- /dev/null +++ b/agent-docs/RAG-user-stories.md @@ -0,0 +1,116 @@ +# Documentation RAG System - User Stories + +## Core User Stories + +### 1. Quick Answer Search +**As a** developer using Agentuity's documentation +**I want to** get quick, accurate answers to my specific questions +**So that** I can solve problems without reading through entire documentation pages + +**Example:** +- "How do I implement streaming responses with OpenAI models?" +- "What's the difference between Agent and AgentRequest?" +- "How do I handle errors in my agent?" + +### 2. Documentation Navigation +**As a** developer exploring Agentuity's documentation +**I want to** find relevant documentation pages based on my topic of interest +**So that** I can learn about features and concepts in a structured way + +**Example:** +- "Show me pages about authentication" +- "Where can I learn about agent templates?" +- "Find documentation about error handling" + +### 3. Code Example Discovery +**As a** developer implementing Agentuity features +**I want to** find relevant code examples quickly +**So that** I can understand how to implement specific functionality + +**Example:** +- "Show me examples of implementing custom tools" +- "How do I structure an agent response?" +- "Find code samples for error handling" + +## User Experience Flows + +### Flow 1: Direct Answer Search +1. User types a specific question in the search bar +2. System returns: + - Direct answer to the question + - Relevant code snippet (if applicable) + - Link to the full documentation page + - Related topics/pages + +### Flow 2: Topic Exploration +1. User searches for a general topic +2. System returns: + - List of relevant documentation pages + - Brief context for each page + - Hierarchical navigation (breadcrumbs) + - Related topics + +### Flow 3: Code Example Search +1. User searches for implementation examples +2. System returns: + - Relevant code snippets + - Context for each example + - Link to full documentation + - Related examples + +## Success Criteria + +### For Quick Answers +- Answers are accurate and up-to-date +- Responses include relevant code snippets when applicable +- Links to full documentation are provided +- Related topics are suggested + +### For Documentation Navigation +- Search results are well-organized +- Breadcrumb navigation is clear +- Related topics are logically connected +- Results are ranked by relevance + +### For Code Examples +- Code snippets are complete and runnable +- Examples include necessary context +- Links to full documentation are provided +- Related examples are suggested + +## Edge Cases to Consider + +1. **Ambiguous Queries** + - User asks a question that could relate to multiple topics + - System should provide disambiguation options + +2. **Out-of-Scope Questions** + - User asks about features not in the documentation + - System should clearly indicate what's not covered + +3. **Technical Depth** + - User might need different levels of technical detail + - System should provide both high-level and detailed answers + +4. **Version-Specific Information** + - User might be using a specific version + - System should indicate version compatibility + +## User Interface Considerations + +### Search Interface +- Global search bar in documentation header +- Clear indication of search scope +- Quick filters for content types (All, Code, Guides, etc.) + +### Results Display +- Clear distinction between direct answers and page references +- Code snippets with syntax highlighting +- Breadcrumb navigation +- Related topics section + +### Navigation +- Easy way to refine search +- Clear path to full documentation +- Related topics suggestions +- History of recent searches \ No newline at end of file diff --git a/agent-docs/README.md b/agent-docs/README.md index 140f6eee..27ba6522 100644 --- a/agent-docs/README.md +++ b/agent-docs/README.md @@ -42,12 +42,8 @@ Follow the interactive prompts to configure your agent. ### Development Mode -Make sure bun packages are properly installed: -```bash -bun install -``` - Run your project in development mode with: + ```bash agentuity dev ``` diff --git a/agent-docs/agentuity.yaml b/agent-docs/agentuity.yaml index e4b99487..89d8f17b 100644 --- a/agent-docs/agentuity.yaml +++ b/agent-docs/agentuity.yaml @@ -78,6 +78,3 @@ agents: - id: agent_9ccc5545e93644bd9d7954e632a55a61 name: doc-qa description: Agent that can answer questions based on dev docs as the knowledge base - - id: agent_ddcb59aa4473f1323be5d9f5fb62b74e - name: agent-pulse - description: Agentuity web app agent that converses with users for generate conversation and structured docs tutorials. diff --git a/agent-docs/bun.lock b/agent-docs/bun.lock index 918d2947..06db8b03 100644 --- a/agent-docs/bun.lock +++ b/agent-docs/bun.lock @@ -12,7 +12,7 @@ "vitest": "^3.2.3", }, "devDependencies": { - "@biomejs/biome": "2.1.2", + "@biomejs/biome": "^1.9.4", "@types/bun": "^1.2.16", }, "peerDependencies": { @@ -33,23 +33,23 @@ "@ai-sdk/ui-utils": ["@ai-sdk/ui-utils@1.2.11", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-3zcwCc8ezzFlwp3ZD15wAPjf2Au4s3vAbKsXQVyhxODHcmu0iyPO2Eua6D/vicq/AUm/BAo60r97O6HU+EI0+w=="], - "@biomejs/biome": ["@biomejs/biome@2.1.2", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.1.2", "@biomejs/cli-darwin-x64": "2.1.2", "@biomejs/cli-linux-arm64": "2.1.2", "@biomejs/cli-linux-arm64-musl": "2.1.2", "@biomejs/cli-linux-x64": "2.1.2", "@biomejs/cli-linux-x64-musl": "2.1.2", "@biomejs/cli-win32-arm64": "2.1.2", "@biomejs/cli-win32-x64": "2.1.2" }, "bin": { "biome": "bin/biome" } }, "sha512-yq8ZZuKuBVDgAS76LWCfFKHSYIAgqkxVB3mGVVpOe2vSkUTs7xG46zXZeNPRNVjiJuw0SZ3+J2rXiYx0RUpfGg=="], + "@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="], - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.1.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-leFAks64PEIjc7MY/cLjE8u5OcfBKkcDB0szxsWUB4aDfemBep1WVKt0qrEyqZBOW8LPHzrFMyDl3FhuuA0E7g=="], + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@1.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw=="], - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.1.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-Nmmv7wRX5Nj7lGmz0FjnWdflJg4zii8Ivruas6PBKzw5SJX/q+Zh2RfnO+bBnuKLXpj8kiI2x2X12otpH6a32A=="], + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@1.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg=="], - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.1.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-NWNy2Diocav61HZiv2enTQykbPP/KrA/baS7JsLSojC7Xxh2nl9IczuvE5UID7+ksRy2e7yH7klm/WkA72G1dw=="], + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g=="], - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.1.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-qgHvafhjH7Oca114FdOScmIKf1DlXT1LqbOrrbR30kQDLFPEOpBG0uzx6MhmsrmhGiCFCr2obDamu+czk+X0HQ=="], + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA=="], - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.1.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Km/UYeVowygTjpX6sGBzlizjakLoMQkxWbruVZSNE6osuSI63i4uCeIL+6q2AJlD3dxoiBJX70dn1enjQnQqwA=="], + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg=="], - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.1.2", "", { "os": "linux", "cpu": "x64" }, "sha512-xlB3mU14ZUa3wzLtXfmk2IMOGL+S0aHFhSix/nssWS/2XlD27q+S6f0dlQ8WOCbYoXcuz8BCM7rCn2lxdTrlQA=="], + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg=="], - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.1.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-G8KWZli5ASOXA3yUQgx+M4pZRv3ND16h77UsdunUL17uYpcL/UC7RkWTdkfvMQvogVsAuz5JUcBDjgZHXxlKoA=="], + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@1.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg=="], - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.1.2", "", { "os": "win32", "cpu": "x64" }, "sha512-9zajnk59PMpjBkty3bK2IrjUsUHvqe9HWwyAWQBjGLE7MIBjbX2vwv1XPEhmO2RRuGoTkVx3WCanHrjAytICLA=="], + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="], "@cfworker/json-schema": ["@cfworker/json-schema@4.1.1", "", {}, "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og=="], diff --git a/agent-docs/package.json b/agent-docs/package.json index 9bc7f850..b1e59c26 100644 --- a/agent-docs/package.json +++ b/agent-docs/package.json @@ -40,4 +40,4 @@ } }, "module": "index.ts" -} \ No newline at end of file +} diff --git a/agent-docs/src/agents/agent-pulse/README.md b/agent-docs/src/agents/agent-pulse/README.md deleted file mode 100644 index 8ff25f7c..00000000 --- a/agent-docs/src/agents/agent-pulse/README.md +++ /dev/null @@ -1,102 +0,0 @@ -# Pulse Agent - -A conversational AI agent for tutorial management built with OpenAI and structured responses. - -## Overview - -Pulse is a friendly AI assistant that helps users discover, start, and navigate through tutorials. It uses OpenAI's GPT-4o-mini with structured response generation to provide both conversational responses and actionable instructions. - -## Architecture - -### Core Components - -- **`index.ts`**: Main agent logic using `generateObject` for structured responses -- **`chat-helpers.ts`**: Conversation history management -- **`tutorial-helpers.ts`**: Tutorial content fetching and formatting -- **`tutorial.ts`**: Tutorial API integration - -### Response Structure - -The agent uses `generateObject` to return structured responses with two parts: - -```typescript -{ - message: string, // Conversational response for the user - actionable?: { // Optional action for the program to execute - type: 'start_tutorial' | 'next_step' | 'previous_step' | 'get_tutorials' | 'none', - tutorialId?: string, - step?: number - } -} -``` - -### How It Works - -1. **User Input**: Agent receives user message and conversation history -2. **LLM Processing**: OpenAI generates structured response with message and optional actionable object -3. **Action Execution**: Program intercepts actionable objects and executes them: - - `get_tutorials`: Fetches available tutorial list - - `start_tutorial`: Fetches real tutorial content from API - - `next_step`/`previous_step`: Navigate through tutorial steps (TODO) -4. **Response**: Returns conversational message plus any additional data (tutorial content, tutorial list, etc.) - -## Key Features - -- **Structured Responses**: Clean separation between conversation and actions -- **Real Tutorial Content**: No hallucinated content - all tutorial data comes from actual APIs -- **Context Awareness**: Maintains conversation history for natural references -- **Extensible Actions**: Easy to add new action types (next step, hints, etc.) -- **Debug Logging**: Comprehensive logging for troubleshooting - -## Example Interactions - -### Starting a Tutorial -**User**: "I want to learn the JavaScript SDK" - -**LLM Response**: -```json -{ - "message": "I'd be happy to help you start the JavaScript SDK tutorial!", - "actionable": { - "type": "start_tutorial", - "tutorialId": "javascript-sdk" - } -} -``` - -**Final Response**: -```json -{ - "response": "I'd be happy to help you start the JavaScript SDK tutorial!", - "tutorialData": { - "type": "tutorial_step", - "tutorialId": "javascript-sdk", - "tutorialTitle": "JavaScript SDK Tutorial", - "currentStep": 1, - "stepContent": "Welcome to the JavaScript SDK tutorial...", - "codeBlock": {...} - }, - "conversationHistory": [...] -} -``` - -### General Conversation -**User**: "What's the difference between TypeScript and JavaScript?" - -**LLM Response**: -```json -{ - "message": "TypeScript is a superset of JavaScript that adds static type checking...", - "actionable": { - "type": "none" - } -} -``` - -## Benefits - -- **Reliable**: No parsing or tool interception needed -- **Extensible**: Easy to add new action types -- **Clean**: Clear separation between conversation and actions -- **Debuggable**: Can see exactly what the LLM wants to do -- **No Hallucination**: Tutorial content comes from real APIs, not LLM generation diff --git a/agent-docs/src/agents/agent-pulse/context/builder.ts b/agent-docs/src/agents/agent-pulse/context/builder.ts deleted file mode 100644 index ca48a4f1..00000000 --- a/agent-docs/src/agents/agent-pulse/context/builder.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { AgentContext } from "@agentuity/sdk"; - -export async function buildSystemPrompt(tutorialContext: string, ctx: AgentContext): Promise { - try { - const systemPrompt = `=== ROLE === -You are Pulse, an AI assistant designed to help developers learn and navigate the Agentuity platform through interactive tutorials and clear guidance. Your primary goal is to assist users with understanding and using the Agentuity SDK effectively. When a user's query is vague, unclear, or lacks specific intent, subtly suggest relevant interactive tutorial to guide them toward learning the platform. For clear, specific questions related to the Agentuity SDK or other topics, provide direct, accurate, and concise answers without mentioning tutorials unless relevant. Always maintain a friendly and approachable tone to encourage engagement. - -Your role is to ensure user have a smooth tutorial experience! - -When user is asking to move to the next tutorial, simply increment the step for them. - -=== PERSONALITY === -- Friendly and encouraging with light humour -- Patient with learners at all levels -- Clear and concise in explanations -- Enthusiastic about teaching and problem-solving - -=== Available Tools or Functions === -You have access to various tools you can use -- use when appropriate! -1. Tutorial management - - startTutorialAtStep: Starting the user off at a specific step of a tutorial. -2. General assistance - - askDocsAgentTool: retrieve Agentuity documentation snippets - -=== TOOL-USAGE RULES (must follow) === -- startTutorialById must only be used when user select a tutorial. If the user starts a new tutorial, the step number should be set to one. Valid step is between 1 and totalSteps of the specific tutorial. -- Treat askDocsAgentTool as a search helper; ignore results you judge irrelevant. - -=== RESPONSE STYLE (format guidelines) === -- Begin with a short answer, then elaborate if necessary. -- Add brief comments to complex code; skip obvious lines. -- End with a question when further clarification could help the user. - -=== SAFETY & BOUNDARIES === -- If asked for private data or secrets, refuse. -- If the user requests actions outside your capabilities, apologise and explain. -- Keep every response < 400 words - -Generate a response to the user query accordingly and try to be helpful - -=== CONTEXT === -${tutorialContext} - -=== END OF PROMPT === - -Stream your reasoning steps clearly.`; - - ctx.logger.debug("Built system prompt with tutorial context"); - return systemPrompt; - } catch (error) { - ctx.logger.error("Failed to build system prompt: %s", error instanceof Error ? error.message : String(error)); - throw error; // Re-throw for centralized handling - } -} \ No newline at end of file diff --git a/agent-docs/src/agents/agent-pulse/index.ts b/agent-docs/src/agents/agent-pulse/index.ts deleted file mode 100644 index 5932c5fc..00000000 --- a/agent-docs/src/agents/agent-pulse/index.ts +++ /dev/null @@ -1,143 +0,0 @@ -import type { AgentRequest, AgentResponse, AgentContext } from "@agentuity/sdk"; -import { streamText } from "ai"; -import { openai } from "@ai-sdk/openai"; -import { createTools } from "./tools"; -import { createAgentState } from "./state"; -import { getTutorialList, type Tutorial } from "./tutorial"; -import { parseAgentRequest } from "./request/parser"; -import { buildSystemPrompt } from "./context/builder"; -import { createStreamingProcessor } from "./streaming/processor"; -import type { ConversationMessage, TutorialState } from "./request/types"; - -/** - * Builds a context string containing available tutorials for the system prompt - */ -async function buildContext( - ctx: AgentContext, - tutorialState?: TutorialState -): Promise { - try { - const tutorials = await getTutorialList(ctx); - - // Handle API failure early - if (!tutorials.success || !tutorials.data) { - ctx.logger.warn("Failed to load tutorial list"); - return defaultFallbackContext(); - } - - const tutorialContent = JSON.stringify(tutorials.data, null, 2); - const currentTutorialInfo = buildCurrentTutorialInfo( - tutorials.data, - tutorialState - ); - - return `===AVAILABLE TUTORIALS==== - - ${tutorialContent} - - ${currentTutorialInfo} - - Note: You should not expose the details of the tutorial IDs to the user. -`; - } catch (error) { - ctx.logger.error("Error building tutorial context: %s", error); - return defaultFallbackContext(); - } -} - -/** - * Builds current tutorial information string if user is in a tutorial - */ -function buildCurrentTutorialInfo( - tutorials: Tutorial[], - tutorialState?: TutorialState -): string { - if (!tutorialState?.tutorialId) { - return ""; - } - - const currentTutorial = tutorials.find( - (t) => t.id === tutorialState.tutorialId - ); - if (!currentTutorial) { - return "\nWarning: User appears to be in an unknown tutorial."; - } - if (tutorialState.currentStep > currentTutorial.totalSteps) { - return `\nUser has completed the tutorial: ${currentTutorial.title} (${currentTutorial.totalSteps} steps)`; - } - return `\nUser is currently on this tutorial: ${currentTutorial.title} (Step ${tutorialState.currentStep} of ${currentTutorial.totalSteps})`; -} - -/** - * Returns fallback context when tutorial list can't be loaded - */ -function defaultFallbackContext(): string { - return `===AVAILABLE TUTORIALS==== -Unable to load tutorial list. Please try again later or contact support.`; -} - -export default async function Agent( - req: AgentRequest, - resp: AgentResponse, - ctx: AgentContext -) { - try { - const parsedRequest = parseAgentRequest(await req.data.json(), ctx); - - // Create state manager - const state = createAgentState(); - - // Build messages for the conversation - const messages: ConversationMessage[] = [ - ...parsedRequest.conversationHistory, - { author: "USER", content: parsedRequest.message }, - ]; - - let tools: any; - let systemPrompt: string = ""; - // Direct LLM access won't require any tools or system prompt - if (!parsedRequest.useDirectLLM) { - // Create tools with state context - tools = await createTools({ - state, - agentContext: ctx, - }); - - // Build tutorial context and system prompt - const tutorialContext = await buildContext( - ctx, - parsedRequest.tutorialData - ); - systemPrompt = await buildSystemPrompt(tutorialContext, ctx); - } - - // Generate streaming response - const result = await streamText({ - model: openai("gpt-4o"), - messages: messages.map((msg) => ({ - role: msg.author === "USER" ? "user" : "assistant", - content: msg.content, - })), - tools, - maxSteps: 3, - system: systemPrompt, - }); - - // Create and return streaming response - const stream = createStreamingProcessor(result, state, ctx); - return resp.stream(stream, "text/event-stream"); - } catch (error) { - ctx.logger.error( - "Agent request failed: %s", - error instanceof Error ? error.message : String(error) - ); - return resp.json( - { - error: - "Sorry, I encountered an error while processing your request. Please try again.", - details: error instanceof Error ? error.message : String(error), - }, - { status: 500 } - ); - } -} diff --git a/agent-docs/src/agents/agent-pulse/request/parser.ts b/agent-docs/src/agents/agent-pulse/request/parser.ts deleted file mode 100644 index 91c5b254..00000000 --- a/agent-docs/src/agents/agent-pulse/request/parser.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { AgentContext } from "@agentuity/sdk"; -import type { ParsedAgentRequest } from "./types"; - -export function parseAgentRequest( - jsonData: any, - ctx: AgentContext -): ParsedAgentRequest { - try { - let message: string = ""; - let conversationHistory: any[] = []; - let tutorialData: any = undefined; - let useDirectLLM = false; - - if (jsonData && typeof jsonData === "object" && !Array.isArray(jsonData)) { - const body = jsonData as any; - message = body.message || ""; - useDirectLLM = body.use_direct_llm || false; - // Process conversation history - if (Array.isArray(body.conversationHistory)) { - conversationHistory = body.conversationHistory.map((msg: any) => { - // Extract only role and content - return { - role: msg.role || (msg.author ? msg.author.toUpperCase() : "USER"), - content: msg.content || "", - }; - }); - } - - tutorialData = body.tutorialData || undefined; - } else { - // Fallback for non-object data - message = String(jsonData || ""); - } - - return { - message, - conversationHistory, - tutorialData, - useDirectLLM, - }; - } catch (error) { - ctx.logger.error( - "Failed to parse agent request: %s", - error instanceof Error ? error.message : String(error) - ); - ctx.logger.debug("Raw request data: %s", JSON.stringify(jsonData)); - throw error; // Re-throw for centralized handling - } -} diff --git a/agent-docs/src/agents/agent-pulse/request/types.ts b/agent-docs/src/agents/agent-pulse/request/types.ts deleted file mode 100644 index 3f14e830..00000000 --- a/agent-docs/src/agents/agent-pulse/request/types.ts +++ /dev/null @@ -1,16 +0,0 @@ -export interface ConversationMessage { - author: "USER" | "ASSISTANT"; - content: string; -} - -export interface TutorialState { - tutorialId: string; - currentStep: number; -} - -export interface ParsedAgentRequest { - message: string; - conversationHistory: ConversationMessage[]; - tutorialData?: TutorialState; - useDirectLLM?: boolean; -} \ No newline at end of file diff --git a/agent-docs/src/agents/agent-pulse/state.ts b/agent-docs/src/agents/agent-pulse/state.ts deleted file mode 100644 index 2cc4bc83..00000000 --- a/agent-docs/src/agents/agent-pulse/state.ts +++ /dev/null @@ -1,46 +0,0 @@ -enum ActionType { - START_TUTORIAL_STEP = "start_tutorial_step" -} - -interface Action { - type: ActionType; - tutorialId: string; - currentStep: number; - totalSteps: number; -} - -interface AgentState { - action: Action | null; - - setAction(action: Action): void; - getAction(): Action | null; - clearAction(): void; - hasAction(): boolean; -} - -class SimpleAgentState implements AgentState { - public action: Action | null = null; - - setAction(action: Action): void { - this.action = action; - } - - getAction(): Action | null { - return this.action; - } - - clearAction(): void { - this.action = null; - } - - hasAction(): boolean { - return this.action !== null; - } -} - -export function createAgentState(): AgentState { - return new SimpleAgentState(); -} - -export type { Action, AgentState }; -export { ActionType }; \ No newline at end of file diff --git a/agent-docs/src/agents/agent-pulse/state/manager.ts b/agent-docs/src/agents/agent-pulse/state/manager.ts deleted file mode 100644 index 00c1c570..00000000 --- a/agent-docs/src/agents/agent-pulse/state/manager.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { AgentContext } from "@agentuity/sdk"; -import { ActionType, type AgentState } from "../state"; -import { getTutorialStep } from "../tutorial"; -import type { TutorialData } from "../streaming/types"; - -export async function handleTutorialState( - state: AgentState, - ctx: AgentContext -): Promise { - try { - if (!state.hasAction()) { - return null; - } - - const action = state.getAction(); - if (!action) { - ctx.logger.warn("No action found in state"); - return null; - } - - ctx.logger.info("Processing action: %s", JSON.stringify(action, null, 2)); - - switch (action.type) { - case ActionType.START_TUTORIAL_STEP: - if (action.tutorialId) { - const tutorialStep = await getTutorialStep(action.tutorialId, action.currentStep, ctx); - if (tutorialStep.success && tutorialStep.data) { - const tutorialData: TutorialData = { - tutorialId: action.tutorialId, - totalSteps: action.totalSteps, - currentStep: action.currentStep, - tutorialStep: { - title: (tutorialStep.data.meta?.title as string) || tutorialStep.data.slug, - mdx: tutorialStep.data.mdx, - snippets: tutorialStep.data.snippets, - totalSteps: action.totalSteps - } - }; - state.clearAction(); - ctx.logger.info("Tutorial state processed successfully"); - return tutorialData; - } - } - break; - default: - ctx.logger.warn("Unknown action type: %s", action.type); - } - - return null; - } catch (error) { - ctx.logger.error("Failed to handle tutorial state: %s", error instanceof Error ? error.message : String(error)); - throw error; // Re-throw for centralized handling - } -} \ No newline at end of file diff --git a/agent-docs/src/agents/agent-pulse/streaming/processor.ts b/agent-docs/src/agents/agent-pulse/streaming/processor.ts deleted file mode 100644 index 72f89019..00000000 --- a/agent-docs/src/agents/agent-pulse/streaming/processor.ts +++ /dev/null @@ -1,115 +0,0 @@ -import type { AgentContext } from "@agentuity/sdk"; -import type { AgentState } from "../state"; -import type { StreamingChunk, TutorialData } from "./types"; -import { handleTutorialState } from "../state/manager"; - -export function createStreamingProcessor( - result: any, - state: AgentState, - ctx: AgentContext -): ReadableStream { - return new ReadableStream({ - async start(controller) { - const encoder = new TextEncoder(); - let accumulatedContent = ""; - - try { - // Stream only safe, user-facing content - for await (const chunk of result.fullStream) { - await processChunk( - chunk, - controller, - encoder, - ctx, - accumulatedContent - ); - } - - // Process tutorial state after streaming text - const finalTutorialData = await handleTutorialState(state, ctx); - - // Send tutorial data if available - if (finalTutorialData) { - sendChunk(controller, encoder, { - type: "tutorial-data", - tutorialData: finalTutorialData, - }); - } - - // Send finish signal - sendChunk(controller, encoder, { type: "finish" }); - - controller.close(); - } catch (error) { - ctx.logger.error( - "Error in streaming response: %s", - error instanceof Error ? error.message : String(error) - ); - sendChunk(controller, encoder, { - type: "error", - error: "Sorry, I encountered an error while processing your request.", - details: error instanceof Error ? error.message : String(error), - }); - controller.close(); - } - }, - }); -} - -async function processChunk( - chunk: any, - controller: ReadableStreamDefaultController, - encoder: TextEncoder, - ctx: AgentContext, - accumulatedContent: string -): Promise { - try { - if (chunk.type === "text-delta") { - accumulatedContent += chunk.textDelta; - sendChunk(controller, encoder, { - type: "text-delta", - textDelta: chunk.textDelta, - }); - } else if (chunk.type === "tool-call") { - const toolName = chunk.toolName || "tool"; - const userFriendlyMessage = getToolStatusMessage(toolName); - sendChunk(controller, encoder, { - type: "status", - message: userFriendlyMessage, - category: "tool", - }); - ctx.logger.debug("Tool called: %s", toolName); - } else if (chunk.type === "reasoning") { - ctx.logger.debug("REASONING: %s", chunk); - } else { - ctx.logger.debug("Skipping chunk type: %s", chunk.type); - ctx.logger.debug(chunk); - } - } catch (error) { - ctx.logger.error( - "Failed to process chunk: %s", - error instanceof Error ? error.message : String(error) - ); - ctx.logger.debug("Chunk data: %s", JSON.stringify(chunk)); - throw error; // Re-throw for centralized handling - } -} - -function sendChunk( - controller: ReadableStreamDefaultController, - encoder: TextEncoder, - chunk: StreamingChunk -): void { - controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`)); -} - -function getToolStatusMessage(toolName: string): string { - switch (toolName) { - case "startTutorialById": - return "Starting tutorial..."; - case "queryOtherAgent": - return "Searching documentation..."; - default: - return "Processing your request..."; - } -} diff --git a/agent-docs/src/agents/agent-pulse/streaming/types.ts b/agent-docs/src/agents/agent-pulse/streaming/types.ts deleted file mode 100644 index 5fcaad52..00000000 --- a/agent-docs/src/agents/agent-pulse/streaming/types.ts +++ /dev/null @@ -1,48 +0,0 @@ -export interface TextDeltaChunk { - type: 'text-delta'; - textDelta: string; -} - -export interface StatusChunk { - type: 'status'; - message: string; - category?: 'tool' | 'search' | 'processing'; -} - -export interface TutorialSnippet { - path: string; - lang?: string; - from?: number; - to?: number; - title?: string; - content: string; -} - -export interface TutorialData { - tutorialId: string; - totalSteps: number; - currentStep: number; - tutorialStep: { - title: string; - mdx: string; - snippets: TutorialSnippet[]; - totalSteps: number; - }; -} - -export interface TutorialDataChunk { - type: 'tutorial-data'; - tutorialData: TutorialData; -} - -export interface ErrorChunk { - type: 'error'; - error: string; - details?: string; -} - -export interface FinishChunk { - type: 'finish'; -} - -export type StreamingChunk = TextDeltaChunk | StatusChunk | TutorialDataChunk | ErrorChunk | FinishChunk; \ No newline at end of file diff --git a/agent-docs/src/agents/agent-pulse/tools.ts b/agent-docs/src/agents/agent-pulse/tools.ts deleted file mode 100644 index a53b20d5..00000000 --- a/agent-docs/src/agents/agent-pulse/tools.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { tool } from "ai"; -import { z } from "zod"; -import { ActionType } from "./state"; -import type { AgentState } from "./state"; -import type { AgentContext } from "@agentuity/sdk"; -import { getTutorialMeta } from "./tutorial"; - -/** - * Context passed to tools for state management and logging - */ -interface ToolContext { - state: AgentState; - agentContext: AgentContext; -} - -/** - * Factory function that creates tools with state management context - */ -export async function createTools(context: ToolContext) { - const { state, agentContext } = context; - const DOC_QA_AGENT_NAME = "doc-qa"; - const docQaAgent = await agentContext.getAgent({ name: DOC_QA_AGENT_NAME }); - /** - * Tool for starting a tutorial - adds action to state queue - */ - const startTutorialAtStep = tool({ - description: "Start a specific tutorial for the user. You must call this function in order for the user to see the tutorial step content. The step number should be between 1 and the total number of steps in the tutorial.", - parameters: z.object({ - tutorialId: z.string().describe("The exact ID of the tutorial to start"), - stepNumber: z.number().describe("The step number of the tutorial to start (1 to total available steps in the tutorial)") - }), - execute: async ({ tutorialId, stepNumber }) => { - // Validate tutorial exists before starting - const tutorialResponse = await getTutorialMeta(tutorialId, agentContext); - if (!tutorialResponse.success || !tutorialResponse.data) { - return `Error fetching tutorial information`; - } - - const data = tutorialResponse.data - const totalSteps = tutorialResponse.data.totalSteps; - if (stepNumber > totalSteps) { - return `This tutorial only has ${totalSteps} steps. You either reached the end of the tutorial or selected an incorrect step number.`; - } - state.setAction({ - type: ActionType.START_TUTORIAL_STEP, - tutorialId: tutorialId, - currentStep: stepNumber, - totalSteps: tutorialResponse.data.totalSteps - }); - agentContext.logger.info("Added start_tutorial action to state for: %s at step %d", tutorialId, stepNumber); - return `Starting "${data.title}". Total steps: ${data.totalSteps} \n\n Description: ${data.description}`; - }, - }); - - /** - * Tool for talking to other agents (nong-tutorial functionality) - * This tool doesn't use state - it returns data directly - */ - const askDocsAgentTool = tool({ - description: "Query the Agentuity Development Documentation agent using RAG (Retrieval Augmented Generation) to get relevant documentation and answers about the Agentuity platform, APIs, and development concepts", - parameters: z.object({ - query: z.string().describe("The question or query to send to the query function"), - }), - execute: async ({ query }) => { - agentContext.logger.info("Querying agent %s with: %s", DOC_QA_AGENT_NAME, query); - const agentPayload = { - message: query, - - } - const response = await docQaAgent.run({ - data: agentPayload, - contentType: 'application/json' - }) - // TODO: handle the docs referencing and inject it to the frontend response - const responseData = await response.data.json(); - return responseData; - }, - }); - - /** - * TODO: This tool allow the agent to get details information about the code execution that the user performed. - */ - const fetchCodeExecutionResultTool = tool({ - description: "Fetch code execution results from the frontend", - parameters: z.object({ - executionId: z.string().describe("The ID of the code execution"), - }), - execute: async ({ executionId }) => { - agentContext.logger.info("Fetching execution result for: %s", executionId); - // This would actually fetch execution results - // For now, just return a mock response - return `Result for execution ${executionId}: console.log('Hello, World!');\n// Output: Hello, World!`; - }, - }); - - // Return tools object - return { - startTutorialById: startTutorialAtStep, - queryOtherAgent: askDocsAgentTool, - }; -} - -export type { ToolContext }; \ No newline at end of file diff --git a/agent-docs/src/agents/agent-pulse/tutorial.ts b/agent-docs/src/agents/agent-pulse/tutorial.ts deleted file mode 100644 index c5a00ed1..00000000 --- a/agent-docs/src/agents/agent-pulse/tutorial.ts +++ /dev/null @@ -1,98 +0,0 @@ -import type { AgentContext } from '@agentuity/sdk'; - -const TUTORIAL_API_BASE_URL = process.env.TUTORIAL_API_URL; - -export interface Tutorial { - id: string; - title: string; - description: string; - totalSteps: number; -} - -interface ApiResponse { - success: boolean; - data?: T; - error?: string; - status?: number; -} - -export interface TutorialSnippet { - path: string; - lang?: string; - from?: number; - to?: number; - title?: string; - content: string; -} - -interface TutorialStepResponseData { - tutorialId: string; - stepNumber: number; - slug: string; - meta: Record; - mdx: string; - snippets: TutorialSnippet[]; -} - -export async function getTutorialList(ctx: AgentContext): Promise> { - try { - const response = await fetch(`${TUTORIAL_API_BASE_URL}/api/tutorials`); - - if (!response.ok) { - const error = await response.json(); - ctx.logger.error('Tutorial API error: %s', error); - throw new Error(`Tutorial API error: ${JSON.stringify(error)}`); - } - - const tutorials = await response.json() as Tutorial[]; - ctx.logger.info('Fetched %d tutorials', tutorials.length); - - return { - success: true, - data: tutorials - }; - } catch (error) { - ctx.logger.error('Error fetching tutorial list: %s', error); - throw error; - } -} - -export async function getTutorialMeta(tutorialId: string, ctx: AgentContext): Promise> { - try { - // New behavior: fetch all, then find by id - const response = await fetch(`${TUTORIAL_API_BASE_URL}/api/tutorials`); - if (!response.ok) { - const error = `Failed to fetch tutorial list: ${response.statusText}`; - ctx.logger.error(error); - return { success: false, status: response.status, error: response.statusText }; - } - const tutorials = (await response.json()) as Tutorial[]; - const found = tutorials.find(t => t.id === tutorialId); - if (!found) { - return { success: false, status: 404, error: 'Tutorial not found' }; - } - return { success: true, data: found }; - } catch (error) { - ctx.logger.error('Error fetching tutorial metadata for %s: %s', tutorialId, error); - throw error; - } -} - -export async function getTutorialStep(tutorialId: string, stepNumber: number, ctx: AgentContext): Promise> { - try { - const response = await fetch(`${TUTORIAL_API_BASE_URL}/api/tutorials/${tutorialId}/steps/${stepNumber}`); - - if (!response.ok) { - ctx.logger.error('Failed to fetch tutorial step %d for tutorial %s: %s', stepNumber, tutorialId, response.statusText); - return { success: false, status: response.status, error: response.statusText }; - } - - const responseData = await response.json(); - ctx.logger.info('Fetched step %d for tutorial %s', stepNumber, tutorialId); - - return responseData as ApiResponse; - } catch (error) { - ctx.logger.error('Error fetching tutorial step %d for tutorial %s: %s', stepNumber, tutorialId, error); - throw error; - } -} \ No newline at end of file diff --git a/agent-docs/src/agents/doc-qa/prompt.ts b/agent-docs/src/agents/doc-qa/prompt.ts index 24aa5f01..cec823fc 100644 --- a/agent-docs/src/agents/doc-qa/prompt.ts +++ b/agent-docs/src/agents/doc-qa/prompt.ts @@ -55,6 +55,7 @@ Return ONLY the query text, nothing else.`; }); const rephrasedQuery = result.text?.trim() || input; + console.log(rephrasedQuery); // Log if we actually rephrased it if (rephrasedQuery !== input) { ctx.logger.info( diff --git a/agent-docs/src/agents/doc-qa/rag.ts b/agent-docs/src/agents/doc-qa/rag.ts index 5e13215e..3140fd26 100644 --- a/agent-docs/src/agents/doc-qa/rag.ts +++ b/agent-docs/src/agents/doc-qa/rag.ts @@ -25,7 +25,8 @@ Your role is to be as helpful as possible and try to assist user by answering th === RULES === 1. Use ONLY the content inside tags to craft your reply. If the required information is missing, state that the docs do not cover it. 2. Never fabricate or guess undocumented details. -3. Focus on answering the QUESTION with the available provided to you. Keep in mind some might not be relevant, so pick the ones that is relevant to the user's question. +3. Focus on answering the QUESTION with the available provided to you. Keep in mind some might not be relevant, + so pick the ones that is relevant to the user's question. 4. Ambiguity handling: • When contains more than one distinct workflow or context that could satisfy the question, do **not** choose for the user. • Briefly (≤ 2 sentences each) summarise each plausible interpretation and ask **one** clarifying question so the user can pick a path. @@ -40,8 +41,8 @@ Your role is to be as helpful as possible and try to assist user by answering th • Use **bold** for important terms and *italic* for emphasis when appropriate. • Use > blockquotes for important notes or warnings. 6. You may suggest concise follow-up questions or related topics that are present in . -7. If do not answer the question, state that explicitly and offer the closest documented topic; answer strictly from or ask one clarifying question if nothing related exists. -8. Keep a neutral, factual tone. +7. Keep a neutral, factual tone. + === OUTPUT FORMAT === Return **valid JSON only** matching this TypeScript type: diff --git a/app/(docs)/[[...slug]]/page.tsx b/app/(docs)/[[...slug]]/page.tsx index 653b12ba..1e04170f 100644 --- a/app/(docs)/[[...slug]]/page.tsx +++ b/app/(docs)/[[...slug]]/page.tsx @@ -19,7 +19,6 @@ import { source } from '@/lib/source'; import { CommunityButton } from '../../../components/Community'; import CopyPageDropdown from '../../../components/CopyPageDropdown'; import { NavButton } from '../../../components/NavButton'; -import CodeFromFiles from '../../../components/CodeFromFiles'; export default async function Page(props: { params: Promise<{ slug?: string[] }>; @@ -58,7 +57,6 @@ export default async function Page(props: { PopupTrigger, CodeExample, CLICommand, - CodeFromFiles, CommunityButton, Mermaid, NavButton, diff --git a/app/api/rag-search/route.ts b/app/api/rag-search/route.ts index 1e8c40ed..665a892f 100644 --- a/app/api/rag-search/route.ts +++ b/app/api/rag-search/route.ts @@ -1,5 +1,5 @@ import type { NextRequest } from 'next/server'; -import { getAgentQaConfig } from '@/lib/env'; +import { getAgentConfig } from '@/lib/env'; import { source } from '@/lib/source'; function documentPathToUrl(docPath: string): string { @@ -93,7 +93,7 @@ export async function GET(request: NextRequest) { } try { - const agentConfig = getAgentQaConfig(); + const agentConfig = getAgentConfig(); // Prepare headers const headers: Record = { diff --git a/app/api/sessions/[sessionId]/messages/route.ts b/app/api/sessions/[sessionId]/messages/route.ts deleted file mode 100644 index 523b43d1..00000000 --- a/app/api/sessions/[sessionId]/messages/route.ts +++ /dev/null @@ -1,374 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { getKVValue, setKVValue } from "@/lib/kv-store"; -import { - Session, - Message, - StreamingChunk, - TutorialData, -} from "@/app/chat/types"; -import { toISOString, getCurrentTimestamp } from "@/app/chat/utils/dateUtils"; -import { getAgentPulseConfig } from "@/lib/env"; -import { config } from "@/lib/config"; -import { parseAndValidateJSON, SessionMessageRequestSchema } from "@/lib/validation/middleware"; - -// Constants -const DEFAULT_CONVERSATION_HISTORY_LIMIT = 10; -const AGENT_REQUEST_TIMEOUT = 30000; // 30 seconds - - -function sanitizeTitle(input: string): string { - if (!input) return ''; - let s = input.trim(); - // Strip wrapping quotes/backticks - if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith('\'') && s.endsWith('\'')) || (s.startsWith('`') && s.endsWith('`'))) { - s = s.slice(1, -1).trim(); - } - // Remove markdown emphasis - s = s.replace(/\*\*([^*]+)\*\*|\*([^*]+)\*|__([^_]+)__|_([^_]+)_/g, (_m, a, b, c, d) => a || b || c || d || ''); - // Remove emojis (basic unicode emoji ranges) - s = s.replace(/[\u{1F300}-\u{1FAFF}\u{1F900}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/gu, ''); - // Collapse whitespace - s = s.replace(/\s+/g, ' ').trim(); - // Sentence case - s = sentenceCase(s); - // Trim trailing punctuation noise - s = s.replace(/[\s\-–—:;,\.]+$/g, '').trim(); - // Enforce 60 chars - if (s.length > 60) s = s.slice(0, 60).trim(); - return s; -} - -function sentenceCase(str: string): string { - if (!str) return ''; - const lower = str.toLowerCase(); - return lower.charAt(0).toUpperCase() + lower.slice(1); -} - -/** - * POST /api/sessions/[sessionId]/messages - Add a message to a session and process with streaming - * - * This endpoint now handles: - * 1. Adding a user message to a session - * 2. Processing the message with the agent - * 3. Streaming the response back to the client - * 4. Saving the assistant's response when complete - */ -export async function POST( - request: NextRequest, - { params }: { params: Promise<{ sessionId: string }> } -) { - try { - const userId = request.cookies.get("chat_user_id")?.value; - if (!userId) { - return NextResponse.json({ error: "User ID not found" }, { status: 401 }); - } - - const paramsData = await params; - const sessionId = paramsData.sessionId; - - const validation = await parseAndValidateJSON(request, SessionMessageRequestSchema); - - if (!validation.success) { - return validation.response; - } - - const { message, processWithAgent } = validation.data; - - // Ensure timestamp is in ISO string format - if (message.timestamp) { - message.timestamp = toISOString(message.timestamp); - } - const sessionKey = `${userId}_${sessionId}`; - const sessionResponse = await getKVValue(sessionKey, { - storeName: config.defaultStoreName, - }); - - // Helper: background title generation and persistence - async function generateAndPersistTitle(sessionId: string, sessionKey: string, finalSession: Session) { - try { - if ((finalSession as any).title) { - return; // Title already set - } - // Build compact conversation history (last 10 messages, truncate content) - const HISTORY_LIMIT = 10; - const MAX_CONTENT_LEN = 400; - const history = finalSession.messages - .slice(-HISTORY_LIMIT) - .map(m => ({ - author: m.author, - content: (m.content || '').slice(0, MAX_CONTENT_LEN), - })); - - const prompt = `Generate a very short session title summarizing the conversation topic.\n\nRequirements:\n- sentence case\n- no emojis\n- <= 60 characters\n- no quotes or markdown\n- output the title only, no extra text`; - - const agentConfig = getAgentPulseConfig(); - const headers: Record = { 'Content-Type': 'application/json' }; - if (agentConfig.bearerToken) headers['Authorization'] = `Bearer ${agentConfig.bearerToken}`; - - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 3000); - let agentResponse: Response | null = null; - try { - agentResponse = await fetch(agentConfig.url, { - method: 'POST', - headers, - body: JSON.stringify({ - message: prompt, - conversationHistory: history, - use_direct_llm: true, - }), - signal: controller.signal, - }); - } finally { - clearTimeout(timeoutId); - } - - if (!agentResponse || !agentResponse.ok) { - console.error(`[title-gen] failed: bad response ${agentResponse ? agentResponse.status : 'no-response'}`); - return; - } - - const reader = agentResponse.body?.getReader(); - if (!reader) { - console.error('[title-gen] failed: no response body'); - return; - } - - let accumulated = ''; - const textDecoder = new TextDecoder(); - while (true) { - const { done, value } = await reader.read(); - if (done) break; - if (value) { - const text = textDecoder.decode(value); - for (const line of text.split('\n')) { - if (line.startsWith('data: ')) { - try { - const ev = JSON.parse(line.slice(6)); - if (ev.type === 'text-delta' && ev.textDelta) accumulated += ev.textDelta; - if (ev.type === 'finish') { - try { await reader.cancel(); } catch { } - break; - } - } catch { } - } - } - } - } - - const candidate = sanitizeTitle(accumulated); - const title = candidate || 'New chat'; - - // Re-fetch and set title only if still empty - const latest = await getKVValue(sessionKey, { storeName: config.defaultStoreName }); - if (!latest.success || !latest.data) return; - const current = latest.data as any; - if (current.title) return; - current.title = title; - await setKVValue(sessionKey, current, { storeName: config.defaultStoreName }); - - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - if (msg.includes('The operation was aborted') || msg.includes('aborted')) { - console.error('[title-gen] timeout after 3000ms'); - } else { - console.error(`[title-gen] failed: ${msg}`); - } - } - } - if (!sessionResponse.success || !sessionResponse.data) { - return NextResponse.json({ error: "Session not found" }, { status: 404 }); - } - - const session = sessionResponse.data; - - const updatedSession: Session = { - ...session, - messages: [...session.messages, message], - }; - - try { - await setKVValue(sessionKey, updatedSession, { - storeName: config.defaultStoreName, - }); - } catch (error) { - console.error( - `Failed to save session after adding message. SessionId: ${sessionId}, Error details:`, - error instanceof Error ? error.message : String(error), - error instanceof Error && error.stack ? `Stack: ${error.stack}` : '' - ); - return NextResponse.json( - { - error: "Failed to save message to session", - details: "Unable to persist the message. Please try again." - }, - { status: 500 } - ); - } - - if (!processWithAgent || message.author !== "USER") { - return NextResponse.json( - { success: true, session: updatedSession }, - { status: 200 } - ); - } - - // Create assistant message placeholder for tracking - const assistantMessageId = crypto.randomUUID(); - - // Process with agent and stream response - const agentConfig = getAgentPulseConfig(); - const agentUrl = agentConfig.url; - - // Get current tutorial state for the user - const { TutorialStateManager } = await import('@/lib/tutorial/state-manager'); - const currentTutorialState = await TutorialStateManager.getCurrentTutorialState(userId); - - const agentPayload = { - message: message.content, - conversationHistory: updatedSession.messages.slice( - -DEFAULT_CONVERSATION_HISTORY_LIMIT - ), - tutorialData: currentTutorialState, - }; - - // Prepare headers with optional bearer token - const headers: Record = { - "Content-Type": "application/json", - }; - if (agentConfig.bearerToken) { - headers["Authorization"] = `Bearer ${agentConfig.bearerToken}`; - } - - // Real agent call (SSE response expected) - const agentResponse = await fetch(agentUrl, { - method: 'POST', - headers, - body: JSON.stringify(agentPayload), - signal: AbortSignal.timeout(AGENT_REQUEST_TIMEOUT), - }); - - if (!agentResponse.ok) { - throw new Error(`Agent responded with status: ${agentResponse.status}`); - } - - // Process streaming response - let accumulatedContent = ""; - let finalTutorialData: TutorialData | undefined = undefined; - - const transformStream = new TransformStream({ - async transform(chunk, controller) { - // Forward the chunk to the client - controller.enqueue(chunk); - - // Process the chunk to accumulate the full response - const text = new TextDecoder().decode(chunk); - const lines = text.split("\n"); - - for (const line of lines) { - if (line.startsWith("data: ")) { - try { - const data = JSON.parse(line.slice(6)) as StreamingChunk; - - if (data.type === "text-delta" && data.textDelta) { - accumulatedContent += data.textDelta; - } else if (data.type === "tutorial-data" && data.tutorialData) { - finalTutorialData = data.tutorialData; - - // Update user's tutorial progress - await TutorialStateManager.updateTutorialProgress( - userId, - finalTutorialData.tutorialId, - finalTutorialData.currentStep, - finalTutorialData.totalSteps - ); - } else if (data.type === "finish") { - // When the stream is finished, save the assistant message - const assistantMessage: Message = { - id: assistantMessageId, - author: "ASSISTANT", - content: accumulatedContent, - timestamp: getCurrentTimestamp(), - tutorialData: finalTutorialData, - }; - - const finalSession = { - ...updatedSession, - messages: [...updatedSession.messages, assistantMessage], - }; - - await setKVValue(sessionKey, finalSession, { - storeName: config.defaultStoreName, - }); - - // Trigger background title generation if missing - // Do not await to avoid delaying the client stream completion - void generateAndPersistTitle(sessionId, sessionKey, finalSession); - - // Send the final session in the finish event - controller.enqueue( - new TextEncoder().encode( - `data: ${JSON.stringify({ - type: "finish", - session: finalSession, - })}\n\n` - ) - ); - } - } catch (error) { - console.error("Error processing stream chunk:", error); - } - } - } - }, - }); - - // Pipe the agent response through our transform stream - const reader = agentResponse.body?.getReader(); - if (!reader) { - throw new Error("No response body from agent"); - } - const writer = transformStream.writable.getWriter(); - (async () => { - try { - while (true) { - const { done, value } = await reader.read(); - if (done) { - break; - } - try { - await writer.write(value); - } catch (writeError) { - console.error('Error writing to transform stream:', writeError); - throw writeError; - } - } - await writer.close(); - } catch (error) { - console.error('Error in stream processing:', error); - writer.abort(error); - } - })(); - - return new NextResponse(transformStream.readable, { - headers: { - "Content-Type": "text/event-stream", - "Cache-Control": "no-cache", - Connection: "keep-alive", - }, - }); - } catch (error) { - console.error("Error in messages API:", error); - // Log the full error stack trace for debugging - if (error instanceof Error) { - console.error("Error stack:", error.stack); - } - return new Response( - JSON.stringify({ - error: "Internal server error", - details: error instanceof Error ? error.message : String(error), - }), - { status: 500, headers: { "Content-Type": "application/json" } } - ); - } -} diff --git a/app/api/sessions/[sessionId]/route.ts b/app/api/sessions/[sessionId]/route.ts deleted file mode 100644 index a3900650..00000000 --- a/app/api/sessions/[sessionId]/route.ts +++ /dev/null @@ -1,266 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { getKVValue, setKVValue, deleteKVValue } from '@/lib/kv-store'; -import { Session, Message, SessionSchema } from '@/app/chat/types'; -import { toISOString } from '@/app/chat/utils/dateUtils'; -import { config } from '@/lib/config'; -import { parseAndValidateJSON, SessionMessageOnlyRequestSchema } from '@/lib/validation/middleware'; - -/** - * GET /api/sessions/[sessionId] - Get a specific session - */ -export async function GET( - request: NextRequest, - { params }: { params: Promise<{ sessionId: string }> } -) { - try { - const userId = request.cookies.get('chat_user_id')?.value; - if (!userId) { - return NextResponse.json({ error: 'User ID not found' }, { status: 401 }); - } - - const paramsData = await params; - const sessionId = paramsData.sessionId; - const sessionKey = `${userId}_${sessionId}`; - const response = await getKVValue(sessionKey, { storeName: config.defaultStoreName }); - - if (!response.success) { - return NextResponse.json( - { error: response.error || 'Session not found' }, - { status: response.statusCode || 404 } - ); - } - - return NextResponse.json({ session: response.data }); - } catch (error) { - return NextResponse.json( - { error: error instanceof Error ? error.message : 'Unknown error occurred' }, - { status: 500 } - ); - } -} - -/** - * PUT /api/sessions/[sessionId] - Update a session - */ -export async function PUT( - request: NextRequest, - { params }: { params: Promise<{ sessionId: string }> } -) { - try { - const userId = request.cookies.get('chat_user_id')?.value; - if (!userId) { - return NextResponse.json({ error: 'User ID not found' }, { status: 401 }); - } - - const paramsData = await params; - const sessionId = paramsData.sessionId; - const sessionKey = `${userId}_${sessionId}`; - - const validation = await parseAndValidateJSON(request, SessionSchema); - if (!validation.success) { - return validation.response; - } - - const session = validation.data; - - if (session.sessionId !== sessionId) { - return NextResponse.json( - { error: 'Session ID mismatch' }, - { status: 400 } - ); - } - - // Process any messages to ensure timestamps are in ISO string format - if (session.messages && session.messages.length > 0) { - session.messages = session.messages.map((message: Message) => { - if (message.timestamp) { - return { - ...message, - timestamp: toISOString(message.timestamp) - }; - } - return message; - }); - } - - // Update the individual session - const response = await setKVValue( - sessionKey, - session, - { storeName: config.defaultStoreName } - ); - - if (!response.success) { - return NextResponse.json( - { error: response.error || 'Failed to update session' }, - { status: response.statusCode || 500 } - ); - } - - // Update the master list if needed (ensure the session ID is in the list) - const allSessionsResponse = await getKVValue(userId, { storeName: config.defaultStoreName }); - const sessionIds = allSessionsResponse.success ? allSessionsResponse.data || [] : []; - - // If the session ID isn't in the list, add it to the beginning - if (!sessionIds.includes(sessionKey)) { - const updatedSessionIds = [sessionKey, ...sessionIds]; - - const sessionsListResponse = await setKVValue( - userId, - updatedSessionIds, - { storeName: config.defaultStoreName } - ); - - if (!sessionsListResponse.success) { - // Log the error but don't fail the request - console.error('Failed to update sessions list:', sessionsListResponse.error); - } - } - - return NextResponse.json({ success: true, session }); - } catch (error) { - return NextResponse.json( - { error: error instanceof Error ? error.message : 'Unknown error occurred' }, - { status: 500 } - ); - } -} - -/** - * DELETE /api/sessions/[sessionId] - Delete a session - */ -export async function DELETE( - request: NextRequest, - { params }: { params: Promise<{ sessionId: string }> } -) { - try { - const userId = request.cookies.get('chat_user_id')?.value; - if (!userId) { - return NextResponse.json({ error: 'User ID not found' }, { status: 401 }); - } - - const paramsData = await params; - const sessionId = paramsData.sessionId; - const sessionKey = `${userId}_${sessionId}`; - // Delete the session data - const sessionResponse = await deleteKVValue( - sessionKey, - { storeName: config.defaultStoreName } - ); - - if (!sessionResponse.success) { - return NextResponse.json( - { error: sessionResponse.error || 'Failed to delete session' }, - { status: sessionResponse.statusCode || 500 } - ); - } - - // Remove from sessions list - const allSessionsResponse = await getKVValue(userId, { storeName: config.defaultStoreName }); - const sessionIds = allSessionsResponse.success ? allSessionsResponse.data || [] : []; - - const updatedSessionIds = sessionIds.filter(id => id !== sessionKey); - - const sessionsListResponse = await setKVValue( - userId, - updatedSessionIds, - { storeName: config.defaultStoreName } - ); - - if (!sessionsListResponse.success) { - return NextResponse.json( - { error: sessionsListResponse.error || 'Failed to update sessions list' }, - { status: sessionsListResponse.statusCode || 500 } - ); - } - - return NextResponse.json({ success: true }); - } catch (error) { - return NextResponse.json( - { error: error instanceof Error ? error.message : 'Unknown error occurred' }, - { status: 500 } - ); - } -} - -/** - * POST /api/sessions/[sessionId]/messages - Add a message to a session - */ -export async function POST( - request: NextRequest, - { params }: { params: Promise<{ sessionId: string }> } -) { - try { - const userId = request.cookies.get('chat_user_id')?.value; - if (!userId) { - return NextResponse.json({ error: 'User ID not found' }, { status: 401 }); - } - - const paramsData = await params; - const sessionId = paramsData.sessionId; - const sessionKey = `${userId}_${sessionId}`; - - const validation = await parseAndValidateJSON(request, SessionMessageOnlyRequestSchema); - - if (!validation.success) { - return validation.response; - } - - const { message } = validation.data; - - // Get current session - const sessionResponse = await getKVValue(sessionKey, { storeName: config.defaultStoreName }); - if (!sessionResponse.success || !sessionResponse.data) { - return NextResponse.json( - { error: 'Session not found' }, - { status: 404 } - ); - } - - const session = sessionResponse.data; - const updatedSession: Session = { - ...session, - messages: [...session.messages, message] - }; - - // Update the individual session - const updateResponse = await setKVValue( - sessionKey, - updatedSession, - { storeName: config.defaultStoreName } - ); - - if (!updateResponse.success) { - return NextResponse.json( - { error: updateResponse.error || 'Failed to update session' }, - { status: updateResponse.statusCode || 500 } - ); - } - - // Move this session ID to the top of the master list (most recently used) - const allSessionsResponse = await getKVValue(userId, { storeName: config.defaultStoreName }); - const sessionIds = allSessionsResponse.success ? allSessionsResponse.data || [] : []; - - // Remove the current session ID if it exists and add it to the beginning - const filteredSessionIds = sessionIds.filter(id => id !== sessionKey); - const updatedSessionIds = [sessionKey, ...filteredSessionIds]; - - const sessionsListResponse = await setKVValue( - userId, - updatedSessionIds, - { storeName: config.defaultStoreName } - ); - - if (!sessionsListResponse.success) { - // Log the error but don't fail the request since we already updated the individual session - console.error('Failed to update sessions list:', sessionsListResponse.error); - } - - return NextResponse.json({ success: true, session: updatedSession }); - } catch (error) { - return NextResponse.json( - { error: error instanceof Error ? error.message : 'Unknown error occurred' }, - { status: 500 } - ); - } -} diff --git a/app/api/sessions/route.ts b/app/api/sessions/route.ts deleted file mode 100644 index bd6a97f2..00000000 --- a/app/api/sessions/route.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { getKVValue, setKVValue } from '@/lib/kv-store'; -import { Session, Message, SessionSchema } from '@/app/chat/types'; -import { toISOString } from '@/app/chat/utils/dateUtils'; -import { config } from '@/lib/config'; -import { parseAndValidateJSON } from '@/lib/validation/middleware'; - -// Constants -const DEFAULT_SESSIONS_LIMIT = 10; -const MAX_SESSIONS_LIMIT = 50; - -/** - * GET /api/sessions - Get all sessions (paginated) - */ -export async function GET(request: NextRequest) { - try { - const userId = request.cookies.get('chat_user_id')?.value; - if (!userId) { - return NextResponse.json({ error: 'User ID not found' }, { status: 401 }); - } - - const searchParams = request.nextUrl.searchParams; - const parsedLimit = Number.parseInt(searchParams.get('limit') ?? String(DEFAULT_SESSIONS_LIMIT)); - const parsedCursor = Number.parseInt(searchParams.get('cursor') ?? '0'); - - const limit = Number.isFinite(parsedLimit) ? Math.min(Math.max(parsedLimit, 1), MAX_SESSIONS_LIMIT) : DEFAULT_SESSIONS_LIMIT; - const cursor = Number.isFinite(parsedCursor) ? Math.max(parsedCursor, 0) : 0; - - const response = await getKVValue(userId, { storeName: config.defaultStoreName }); - if (!response.success) { - if (response.statusCode === 404) { - return NextResponse.json({ sessions: [], pagination: { cursor, nextCursor: null, hasMore: false, total: 0, limit } }); - } - return NextResponse.json( - { error: response.error || 'Failed to retrieve sessions' }, - { status: response.statusCode || 500 } - ); - } - - if (!response.data?.length) { - return NextResponse.json({ sessions: [], pagination: { cursor, nextCursor: null, hasMore: false, total: 0, limit } }); - } - - const sessionIds = response.data; - const total = sessionIds.length; - - const start = Math.min(cursor, total); - const end = Math.min(start + limit, total); - const pageIds = sessionIds.slice(start, end); - - const sessionPromises = pageIds.map(sessionId => getKVValue(sessionId, { storeName: config.defaultStoreName })); - const sessionResults = await Promise.all(sessionPromises); - const sessions = sessionResults - .filter(result => result.success && result.data) - .map(result => result.data as Session); - - const hasMore = end < total; - const nextCursor = hasMore ? end : null; - - return NextResponse.json({ - sessions, - pagination: { cursor: start, nextCursor, hasMore, total, limit } - }); - } catch (error) { - return NextResponse.json( - { error: error instanceof Error ? error.message : 'Unknown error occurred' }, - { status: 500 } - ); - } -} - -/** - * POST /api/sessions - Create a new session - */ -export async function POST(request: NextRequest) { - try { - const userId = request.cookies.get('chat_user_id')?.value; - if (!userId) { - return NextResponse.json({ error: 'User ID not found' }, { status: 401 }); - } - - const validation = await parseAndValidateJSON(request, SessionSchema); - if (!validation.success) { - return validation.response; - } - - const session = validation.data; - - // Process any messages to ensure timestamps are in ISO string format - if (session.messages && session.messages.length > 0) { - session.messages = session.messages.map((message: Message) => { - if (message.timestamp) { - return { - ...message, - timestamp: toISOString(message.timestamp) - }; - } - return message; - }); - } - - const sessionKey = `${userId}_${session.sessionId}`; - - // Save the session data - const sessionResponse = await setKVValue( - sessionKey, - session, - { storeName: config.defaultStoreName } - ); - - if (!sessionResponse.success) { - return NextResponse.json( - { error: sessionResponse.error || 'Failed to create session' }, - { status: sessionResponse.statusCode || 500 } - ); - } - - // Update the sessions list with just the session ID - const allSessionsResponse = await getKVValue(userId, { storeName: config.defaultStoreName }); - const sessionIds = allSessionsResponse.success ? allSessionsResponse.data || [] : []; - - // Add the new session ID to the beginning of the array - const updatedSessionIds = [sessionKey, ...sessionIds.filter(id => id !== sessionKey)]; - - const sessionsListResponse = await setKVValue( - userId, - updatedSessionIds, - { storeName: config.defaultStoreName } - ); - - if (!sessionsListResponse.success) { - return NextResponse.json( - { error: sessionsListResponse.error || 'Failed to update sessions list' }, - { status: sessionsListResponse.statusCode || 500 } - ); - } - - return NextResponse.json({ - success: true, - session, - ...(session.title ? {} : { titleGeneration: 'pending' }) - }); - } catch (error) { - return NextResponse.json( - { error: error instanceof Error ? error.message : 'Unknown error occurred' }, - { status: 500 } - ); - } -} diff --git a/app/api/tutorials/[id]/route.ts b/app/api/tutorials/[id]/route.ts deleted file mode 100644 index 637fb1a3..00000000 --- a/app/api/tutorials/[id]/route.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { readAllTutorials } from '@/lib/tutorial/all-tutorials-reader'; -import { resolve } from 'path'; - -interface RouteParams { - params: Promise<{ id: string }>; -} - -export async function GET(request: NextRequest, { params }: RouteParams) { - try { - const { id } = await params; - const basePath = resolve(process.cwd()); - - // Read all tutorials and find the one with matching ID from meta.json - const allTutorials = await readAllTutorials(basePath); - const tutorial = allTutorials.find(t => t.id === id); - - if (!tutorial) { - return NextResponse.json( - { - success: false, - error: 'Tutorial not found' - }, - { status: 404 } - ); - } - - return NextResponse.json({ - success: true, - data: tutorial - }); - } catch (error) { - console.error('Error reading tutorial:', error); - return NextResponse.json( - { - success: false, - error: 'Failed to read tutorial', - message: error instanceof Error ? error.message : String(error) - }, - { status: 500 } - ); - } -} \ No newline at end of file diff --git a/app/api/tutorials/[id]/steps/[stepNumber]/route.ts b/app/api/tutorials/[id]/steps/[stepNumber]/route.ts deleted file mode 100644 index e949733d..00000000 --- a/app/api/tutorials/[id]/steps/[stepNumber]/route.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { join, resolve, sep } from 'path'; -import { readFile } from 'fs/promises'; -import matter from 'gray-matter'; -import { validateTutorialId, validateStepNumber, createValidationError } from '@/lib/validation/middleware'; - -interface RouteParams { - params: Promise<{ id: string; stepNumber: string }>; -} - -export async function GET(request: NextRequest, { params }: RouteParams) { - try { - const { id, stepNumber } = await params; - - const idValidation = validateTutorialId(id); - if (!idValidation.success) { - return createValidationError('Invalid tutorial ID', idValidation.errors || []); - } - - const stepValidation = validateStepNumber(stepNumber); - if (!stepValidation.success) { - return createValidationError('Invalid step number', stepValidation.errors || []); - } - - const stepIndex = stepValidation.data; - if (!stepIndex) { - return NextResponse.json( - { success: false, error: 'Invalid step number' }, - { status: 400 } - ); - } - - const repoRoot = process.cwd(); - const tutorialDir = join(repoRoot, 'content', 'Tutorial', id); - - // Load child tutorial meta.json to get ordered pages - const childMetaRaw = await readFile(join(tutorialDir, 'meta.json'), 'utf-8'); - const childMeta = JSON.parse(childMetaRaw) as { title?: string; pages?: string[] }; - const pages = (childMeta.pages ?? []).filter(Boolean); - - // Filter out index; map to actual MDX files - const stepSlugs = pages.filter(p => p !== 'index'); - - if (stepIndex < 1 || stepIndex > stepSlugs.length) { - return NextResponse.json( - { success: false, error: 'Step not found' }, - { status: 404 } - ); - } - - const slug = stepSlugs[stepIndex - 1]; - if (!slug) { - return NextResponse.json( - { success: false, error: 'Step not found' }, - { status: 404 } - ); - } - - const mdxPath = join(tutorialDir, `${slug}.mdx`); - const mdxRaw = await readFile(mdxPath, 'utf-8'); - const parsed = matter(mdxRaw); - - // Extract CodeFromFiles tags and resolve their snippet arrays - const snippets: Array<{ path: string; lang?: string; from?: number; to?: number; title?: string; content: string }> = []; - - // Helper to load a snippet descriptor into content - async function loadSnippet(desc: { path: string; lang?: string; from?: number; to?: number; title?: string }) { - const filePath = desc.path; - if (!filePath || !filePath.startsWith('/examples/')) return; - - // Resolve against repo root and ensure containment within /examples - const resolvedPath = resolve(repoRoot, `.${filePath}`); - const examplesBase = resolve(repoRoot, 'examples'); - const isContained = resolvedPath === examplesBase || resolvedPath.startsWith(examplesBase + sep); - if (!isContained) return; - - try { - const fileRaw = await readFile(resolvedPath, 'utf-8'); - const lines = fileRaw.split(/\r?\n/); - const startIdx = Math.max(0, (desc.from ? desc.from - 1 : 0)); - const endIdx = Math.min(lines.length, desc.to ? desc.to : lines.length); - const content = lines.slice(startIdx, endIdx).join('\n'); - snippets.push({ ...desc, content }); - } catch (error) { - console.warn(`Failed to load snippet from ${filePath}:`, error); - } - } - - // 1) Parse blocks - // Robust parser that balances braces to extract snippets={[ ... ]} - const filesTagRegex = /]*?)\/>/g; - let filesTagMatch: RegExpExecArray | null; - while ((filesTagMatch = filesTagRegex.exec(parsed.content)) !== null) { - const propsSrc: string = filesTagMatch[1] || ''; - - const key = 'snippets={'; - const start = propsSrc.indexOf(key); - if (start < 0) continue; - let i = start + key.length; // position after '{' - let depth = 1; - // Scan until matching closing '}' for the snippets prop - while (i < propsSrc.length && depth > 0) { - const ch = propsSrc[i]; - if (ch === '{') depth++; - else if (ch === '}') depth--; - i++; - } - // slice without outer braces - const inner = propsSrc.slice(start + key.length, i - 1).trim(); // should be array source like "[{...},{...}]" - - // Extract object literals from the array by balancing braces again - const objects: string[] = []; - let j = 0; - while (j < inner.length) { - if (inner[j] === '{') { - let d = 1; - let k = j + 1; - while (k < inner.length && d > 0) { - const ch = inner[k]; - if (ch === '{') d++; - else if (ch === '}') d--; - k++; - } - objects.push(inner.slice(j, k)); - j = k; - } else { - j++; - } - } - - for (const objSrc of objects) { - const getStr = (name: string): string | undefined => { - const r = new RegExp(name + '\\s*:\\s*"([^"]*)"'); - const mm = r.exec(objSrc); - return mm ? mm[1] : undefined; - }; - const getNum = (name: string): number | undefined => { - const r = new RegExp(name + '\\s*:\\s*(\\d+)'); - const mm = r.exec(objSrc); - return mm ? Number(mm[1]) : undefined; - }; - const desc = { - path: getStr('path') || '', - lang: getStr('lang'), - from: getNum('from'), - to: getNum('to'), - title: getStr('title') - }; - if (desc.path) { - await loadSnippet(desc); - } - } - } - - return NextResponse.json({ - success: true, - data: { - tutorialId: id, - stepNumber: stepIndex, - slug, - meta: parsed.data ?? {}, - mdx: parsed.content, - snippets, - totalSteps: pages.length - } - }); - } catch (error) { - console.error('Error reading tutorial step:', error); - return NextResponse.json( - { - success: false, - error: 'Failed to read tutorial step', - message: error instanceof Error ? error.message : String(error) - }, - { status: 500 } - ); - } -} \ No newline at end of file diff --git a/app/api/tutorials/route.ts b/app/api/tutorials/route.ts deleted file mode 100644 index 62318f33..00000000 --- a/app/api/tutorials/route.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { join } from 'path'; -import { readFile, stat } from 'fs/promises'; -import matter from 'gray-matter'; - -export async function GET(request: NextRequest) { - try { - const repoRoot = process.cwd(); - const tutorialRoot = join(repoRoot, 'content', 'Tutorial'); - - // Use parent meta.json to control order and which tutorials show up - let pages: string[] = []; - try { - const parentMetaPath = join(tutorialRoot, 'meta.json'); - const parentMetaRaw = await readFile(parentMetaPath, 'utf-8'); - const parentMeta = JSON.parse(parentMetaRaw) as { title?: string; pages?: string[] }; - pages = (parentMeta.pages ?? []).filter(Boolean); - } catch { - // If meta.json doesn't exist or is invalid, return empty array - return NextResponse.json([]); - } - - // If no tutorials configured, return empty array - if (pages.length === 0) { - return NextResponse.json([]); - } - - const results: Array<{ id: string; title: string; description?: string; totalSteps: number }> = []; - - for (const entry of pages) { - if (entry.includes('..') || entry.includes('/') || entry.includes('\\')) { - continue; - } - - const dirPath = join(tutorialRoot, entry); - const filePath = join(tutorialRoot, `${entry}.mdx`); - try { - const st = await stat(dirPath).catch(() => null); - if (st && st.isDirectory()) { - // Directory tutorial: read its meta.json and index.mdx (if present) - const childMetaRaw = await readFile(join(dirPath, 'meta.json'), 'utf-8'); - const childMeta = JSON.parse(childMetaRaw) as { title?: string; pages?: string[] }; - - const pagesList = childMeta.pages ?? []; - const totalSteps = pagesList.filter(p => p !== 'index').length || 0; - - let description: string | undefined; - try { - const idxPath = join(dirPath, 'index.mdx'); - const idxRaw = await readFile(idxPath, 'utf-8'); - const idx = matter(idxRaw); - if (typeof idx.data?.description === 'string') description = idx.data.description; - } catch { - // ignore if index missing - } - - results.push({ - id: entry, - title: childMeta.title || entry, - description, - totalSteps - }); - } else { - // Single-file tutorial - const mdxRaw = await readFile(filePath, 'utf-8'); - const fm = matter(mdxRaw); - const title = (fm.data?.title as string) || entry; - const description = typeof fm.data?.description === 'string' ? (fm.data.description as string) : undefined; - results.push({ id: entry, title, description, totalSteps: 1 }); - } - } catch (err) { - // Skip malformed entries but continue - // eslint-disable-next-line no-console - console.warn(`Skipping tutorial entry ${entry}:`, err); - } - } - - return NextResponse.json(results); - } catch (error) { - console.error('Error reading tutorials:', error); - - // Return proper HTTP error status with minimal error info - return NextResponse.json( - { error: 'Failed to read tutorials' }, - { status: 500 } - ); - } -} \ No newline at end of file diff --git a/app/api/users/tutorial-state/route.ts b/app/api/users/tutorial-state/route.ts deleted file mode 100644 index 70557fa7..00000000 --- a/app/api/users/tutorial-state/route.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { TutorialStateManager } from '@/lib/tutorial/state-manager'; -import { setKVValue } from '@/lib/kv-store'; -import { config } from '@/lib/config'; -import { - parseAndValidateJSON, - TutorialProgressRequestSchema, - TutorialResetRequestSchema -} from '@/lib/validation/middleware'; - -/** - * GET /api/users/tutorial-state - Get current user's tutorial state - */ -export async function GET(request: NextRequest) { - try { - const userId = request.cookies.get('chat_user_id')?.value; - if (!userId) { - return NextResponse.json({ error: 'User ID not found' }, { status: 401 }); - } - - const tutorialState = await TutorialStateManager.getUserTutorialState(userId); - - return NextResponse.json({ - success: true, - data: tutorialState - }); - } catch (error) { - console.error('Error getting tutorial state:', error); - return NextResponse.json( - { error: 'Failed to get tutorial state' }, - { status: 500 } - ); - } -} - -/** - * POST /api/users/tutorial-state - Update tutorial progress - */ -export async function POST(request: NextRequest) { - try { - const userId = request.cookies.get('chat_user_id')?.value; - if (!userId) { - return NextResponse.json({ error: 'User ID not found' }, { status: 401 }); - } - - const validation = await parseAndValidateJSON(request, TutorialProgressRequestSchema); - if (!validation.success) { - return validation.response; - } - - const { tutorialId, currentStep, totalSteps } = validation.data; - - await TutorialStateManager.updateTutorialProgress( - userId, - tutorialId, - currentStep, - totalSteps - ); - - return NextResponse.json({ - success: true, - message: 'Tutorial progress updated' - }); - } catch (error) { - console.error('Error updating tutorial state:', error); - return NextResponse.json( - { error: 'Failed to update tutorial state' }, - { status: 500 } - ); - } -} - -/** - * DELETE /api/users/tutorial-state - Reset tutorial progress - */ -export async function DELETE(request: NextRequest) { - try { - const userId = request.cookies.get('chat_user_id')?.value; - if (!userId) { - return NextResponse.json({ error: 'User ID not found' }, { status: 401 }); - } - - const validation = await parseAndValidateJSON(request, TutorialResetRequestSchema); - if (!validation.success) { - return validation.response; - } - - const { tutorialId } = validation.data; - - const state = await TutorialStateManager.getUserTutorialState(userId); - if (!state.tutorials) { - state.tutorials = {}; - } - delete state.tutorials[tutorialId]; - - // Save the updated state - const kvResponse = await setKVValue(`tutorial_state_${userId}`, state, { - storeName: config.defaultStoreName - }); - - if (!kvResponse.success) { - return NextResponse.json( - { error: kvResponse.error || 'Failed to reset tutorial state' }, - { status: kvResponse.statusCode || 500 } - ); - } - - return NextResponse.json({ - success: true, - message: 'Tutorial progress reset' - }); - } catch (error) { - console.error('Error resetting tutorial state:', error); - return NextResponse.json( - { error: 'Failed to reset tutorial state' }, - { status: 500 } - ); - } -} diff --git a/app/chat/SessionContext.tsx b/app/chat/SessionContext.tsx deleted file mode 100644 index a4a3b42b..00000000 --- a/app/chat/SessionContext.tsx +++ /dev/null @@ -1,21 +0,0 @@ -'use client'; -import { createContext, useContext } from 'react'; -import { Session } from './types'; - -interface SessionContextType { - sessions: Session[]; - setSessions: (updater: React.SetStateAction, options?: { revalidate: boolean }) => void; - currentSessionId: string; - // A simple trigger to revalidate sessions; implementation may vary under the hood - revalidateSessions?: () => void | Promise; -} - -export const SessionContext = createContext(undefined); - -export const useSessions = () => { - const context = useContext(SessionContext); - if (!context) { - throw new Error('useSessions must be used within a SessionProvider'); - } - return context; -}; diff --git a/app/chat/[sessionId]/page.tsx b/app/chat/[sessionId]/page.tsx deleted file mode 100644 index ef569ef4..00000000 --- a/app/chat/[sessionId]/page.tsx +++ /dev/null @@ -1,233 +0,0 @@ -'use client'; - -import { useEffect, useState } from 'react'; -import { useParams } from "next/navigation"; -import { Allotment } from "allotment"; -import "allotment/dist/style.css"; -import { v4 as uuidv4 } from 'uuid'; -import { ChatMessagesArea } from '../components/ChatMessagesArea'; -import { CodeEditor } from '../components/CodeEditor'; -import { Session, Message } from '../types'; -import { useSessions } from '../SessionContext'; -import { sessionService } from '../services/sessionService'; -import { Skeleton } from '@/components/ui/skeleton'; - -export default function ChatSessionPage() { - const { sessionId } = useParams<{ sessionId: string }>(); - const [session, setSession] = useState(); - const [editorOpen, setEditorOpen] = useState(false); - const [editorContent, setEditorContent] = useState(''); - const { sessions, setSessions, revalidateSessions } = useSessions(); - const [creationError, setCreationError] = useState(null); - - - const handleSendMessage = async (content: string, sessionId: string) => { - if (!content || !sessionId) return; - - const newMessage: Message = { - id: uuidv4(), - author: 'USER', - content: content, - timestamp: new Date().toISOString() - }; - - const assistantMessage: Message = { - id: uuidv4(), - author: 'ASSISTANT', - content: '', - timestamp: new Date().toISOString() - }; - - try { - setSession(prevSession => { - if (!prevSession) return prevSession; - return { - ...prevSession, - messages: [...prevSession.messages, newMessage, assistantMessage] - }; - }); - - await sessionService.addMessageToSessionStreaming( - sessionId, - newMessage, - { - onTextDelta: (textDelta) => { - setSession(prev => { - if (!prev) return prev; - const updatedMessages = prev.messages.map(msg => { - if (msg.id === assistantMessage.id) { - return { - ...msg, - content: msg.content + textDelta - }; - } - return msg; - }); - return { ...prev, messages: updatedMessages }; - }); - }, - - onTutorialData: (tutorialData) => { - setSession(prev => { - if (!prev) return prev; - const updatedMessages = prev.messages.map(msg => - msg.id === assistantMessage.id - ? { ...msg, tutorialData: tutorialData } - : msg - ); - return { ...prev, messages: updatedMessages }; - }); - }, - - onFinish: (finalSession) => { - setSession(finalSession); - setSessions(prev => prev.map(s => s.sessionId === sessionId ? finalSession : s)); - }, - - onError: (error) => { - console.error('Error sending message:', error); - setSession(prev => { - if (!prev) return prev; - const updatedMessages = prev.messages.map(msg => - msg.id === assistantMessage.id - ? { ...msg, content: 'Sorry, I encountered an error. Please try again.' } - : msg - ); - return { ...prev, messages: updatedMessages }; - }); - } - } - ); - - } catch (error) { - console.error('Error sending message:', error); - setSession(prevSession => { - if (!prevSession) return prevSession; - const filteredMessages = prevSession.messages.filter(msg => - msg.id !== newMessage.id && msg.id !== assistantMessage.id - ); - return { ...prevSession, messages: filteredMessages }; - }); - } - }; - - useEffect(() => { - const foundSession = sessions.find(s => s.sessionId === sessionId); - if (foundSession) { - setSession(foundSession); - return; - } - - const storageKey = `initialMessage:${sessionId}`; - const initialMessage = sessionStorage.getItem(storageKey); - if (!initialMessage) { - return; - } - sessionStorage.removeItem(storageKey); - - const userMessage: Message = { - id: uuidv4(), - author: 'USER', - content: initialMessage, - timestamp: new Date().toISOString(), - }; - const assistantPlaceholder: Message = { - id: uuidv4(), - author: 'ASSISTANT', - content: '', - timestamp: new Date().toISOString(), - }; - const temporarySession: Session = { - sessionId: sessionId as string, - messages: [userMessage, assistantPlaceholder], - }; - - setSession(temporarySession); - - sessionService.createSession({ - sessionId: sessionId as string, - messages: [] - }) - .then(async response => { - if (response.success && response.data) { - setSession(response.data); - await handleSendMessage(initialMessage, sessionId); - } else { - setCreationError(response.error || 'Failed to create session'); - revalidateSessions?.(); - } - }) - .catch(error => { - setCreationError(error.message || 'Error creating session'); - revalidateSessions?.(); - }); - }, [sessionId]); - - - const toggleEditor = () => { setEditorOpen(false) }; - const stopServer = () => { }; - - return ( -
- {/* Non-blocking error banner */} - {creationError && ( -
-
- Error creating session: {creationError} - -
-
- )} - - - -
-
- {session ? ( - { setEditorOpen(true) }} - /> - ) : ( -
- -
- {Array.from({ length: 5 }).map((_, i) => ( -
- - -
- ))} -
-
- )} -
-
-
- {editorOpen && ( - -
- { }} - stopServer={stopServer} - editorContent={editorContent} - setEditorContent={setEditorContent} - toggleEditor={toggleEditor} - /> -
-
- )} -
-
- ); -} \ No newline at end of file diff --git a/app/chat/components/ChatInput.tsx b/app/chat/components/ChatInput.tsx deleted file mode 100644 index 0422b2ac..00000000 --- a/app/chat/components/ChatInput.tsx +++ /dev/null @@ -1,74 +0,0 @@ -'use client'; - -import { useEffect, KeyboardEvent, useState } from 'react'; -import { Send } from 'lucide-react'; -import { useAutoResize } from '../utils/useAutoResize'; - -interface ChatInputProps { - loading?: boolean; - onSendMessage: (message: string) => void; -} - -export function ChatInput({ - loading = false, - onSendMessage -}: ChatInputProps) { - const [currentInput, setCurrentInput] = useState(''); - const { textareaRef } = useAutoResize(currentInput, { maxHeight: 320 }); - - useEffect(() => { - textareaRef.current?.focus(); - }, []); - - useEffect(() => { - if (!loading) { - textareaRef.current?.focus(); - } - }, [loading]); - - const handleSend = () => { - if (currentInput.trim() && !loading) { - onSendMessage(currentInput.trim()); - setCurrentInput(''); - } - }; - - const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Enter' && (!e.shiftKey || e.ctrlKey || e.metaKey)) { - e.preventDefault(); - handleSend(); - } - }; - - return ( -
- {/* Textarea Container */} -
-
-