diff --git a/.changeset/chilly-bats-smile.md b/.changeset/chilly-bats-smile.md new file mode 100644 index 000000000..03d908fc3 --- /dev/null +++ b/.changeset/chilly-bats-smile.md @@ -0,0 +1,5 @@ +--- +"create-llama": patch +--- + +Fix the error: Unable to view file sources due to CORS. diff --git a/templates/components/agents/python/deep_research/app/workflows/agents.py b/templates/components/agents/python/deep_research/app/workflows/agents.py index 8d32db75c..20b82b6f7 100644 --- a/templates/components/agents/python/deep_research/app/workflows/agents.py +++ b/templates/components/agents/python/deep_research/app/workflows/agents.py @@ -16,7 +16,10 @@ class AnalysisDecision(BaseModel): description="Whether to continue research, write a report, or cancel the research after several retries" ) research_questions: Optional[List[str]] = Field( - description="Questions to research if continuing research. Maximum 3 questions. Set to null or empty if writing a report.", + description=""" + If the decision is to research, provide a list of questions to research that related to the user request. + Maximum 3 questions. Set to null or empty if writing a report or cancel the research. + """, default_factory=list, ) cancel_reason: Optional[str] = Field( @@ -29,23 +32,23 @@ async def plan_research( memory: SimpleComposableMemory, context_nodes: List[Node], user_request: str, + total_questions: int, ) -> AnalysisDecision: - analyze_prompt = PromptTemplate( - """ + analyze_prompt = """ You are a professor who is guiding a researcher to research a specific request/problem. Your task is to decide on a research plan for the researcher. + The possible actions are: + Provide a list of questions for the researcher to investigate, with the purpose of clarifying the request. + Write a report if the researcher has already gathered enough research on the topic and can resolve the initial request. + Cancel the research if most of the answers from researchers indicate there is insufficient information to research the request. Do not attempt more than 3 research iterations or too many questions. + The workflow should be: + Always begin by providing some initial questions for the researcher to investigate. + Analyze the provided answers against the initial topic/request. If the answers are insufficient to resolve the initial request, provide additional questions for the researcher to investigate. + If the answers are sufficient to resolve the initial request, instruct the researcher to write a report. - - {user_request} - + Here are the context: {context_str} @@ -53,8 +56,29 @@ async def plan_research( {conversation_context} + + {enhanced_prompt} + + Now, provide your decision in the required format for this user request: + + {user_request} + """ - ) + # Manually craft the prompt to avoid LLM hallucination + enhanced_prompt = "" + if total_questions == 0: + # Avoid writing a report without any research context + enhanced_prompt = """ + + The student has no questions to research. Let start by asking some questions. + """ + elif total_questions > 6: + # Avoid asking too many questions (when the data is not ready for writing a report) + enhanced_prompt = f""" + + The student has researched {total_questions} questions. Should cancel the research if the context is not enough to write a report. + """ + conversation_context = "\n".join( [f"{message.role}: {message.content}" for message in memory.get_all()] ) @@ -63,10 +87,11 @@ async def plan_research( ) res = await Settings.llm.astructured_predict( output_cls=AnalysisDecision, - prompt=analyze_prompt, + prompt=PromptTemplate(template=analyze_prompt), user_request=user_request, context_str=context_str, conversation_context=conversation_context, + enhanced_prompt=enhanced_prompt, ) return res diff --git a/templates/components/agents/python/deep_research/app/workflows/deep_research.py b/templates/components/agents/python/deep_research/app/workflows/deep_research.py index e739fe120..59648b524 100644 --- a/templates/components/agents/python/deep_research/app/workflows/deep_research.py +++ b/templates/components/agents/python/deep_research/app/workflows/deep_research.py @@ -89,10 +89,11 @@ def __init__( ) @step - def retrieve(self, ctx: Context, ev: StartEvent) -> PlanResearchEvent: + async def retrieve(self, ctx: Context, ev: StartEvent) -> PlanResearchEvent: """ Initiate the workflow: memory, tools, agent """ + await ctx.set("total_questions", 0) self.user_request = ev.get("input") self.memory.put_messages( messages=[ @@ -132,9 +133,7 @@ def retrieve(self, ctx: Context, ev: StartEvent) -> PlanResearchEvent: nodes=nodes, ) ) - return PlanResearchEvent( - context_nodes=self.context_nodes, - ) + return PlanResearchEvent() @step async def analyze( @@ -153,10 +152,12 @@ async def analyze( }, ) ) + total_questions = await ctx.get("total_questions") res = await plan_research( memory=self.memory, context_nodes=self.context_nodes, user_request=self.user_request, + total_questions=total_questions, ) if res.decision == "cancel": ctx.write_event_to_stream( @@ -172,6 +173,22 @@ async def analyze( result=res.cancel_reason, ) elif res.decision == "write": + # Writing a report without any research context is not allowed. + # It's a LLM hallucination. + if total_questions == 0: + ctx.write_event_to_stream( + DataEvent( + type="deep_research_event", + data={ + "event": "analyze", + "state": "done", + }, + ) + ) + return StopEvent( + result="Sorry, I have a problem when analyzing the retrieved information. Please try again.", + ) + self.memory.put( message=ChatMessage( role=MessageRole.ASSISTANT, @@ -180,7 +197,11 @@ async def analyze( ) ctx.send_event(ReportEvent()) else: - await ctx.set("n_questions", len(res.research_questions)) + total_questions += len(res.research_questions) + await ctx.set("total_questions", total_questions) # For tracking + await ctx.set( + "waiting_questions", len(res.research_questions) + ) # For waiting questions to be answered self.memory.put( message=ChatMessage( role=MessageRole.ASSISTANT, @@ -270,7 +291,7 @@ async def collect_answers( """ Collect answers to all questions """ - num_questions = await ctx.get("n_questions") + num_questions = await ctx.get("waiting_questions") results = ctx.collect_events( ev, expected=[CollectAnswersEvent] * num_questions, @@ -284,7 +305,7 @@ async def collect_answers( content=f"{result.question}\n{result.answer}", ) ) - await ctx.set("n_questions", 0) + await ctx.set("waiting_questions", 0) self.memory.put( message=ChatMessage( role=MessageRole.ASSISTANT, diff --git a/templates/types/streaming/fastapi/main.py b/templates/types/streaming/fastapi/main.py index 81e90a446..a19c4bb27 100644 --- a/templates/types/streaming/fastapi/main.py +++ b/templates/types/streaming/fastapi/main.py @@ -1,20 +1,23 @@ # flake8: noqa: E402 -from app.config import DATA_DIR, STATIC_DIR from dotenv import load_dotenv +from app.config import DATA_DIR, STATIC_DIR + load_dotenv() import logging import os import uvicorn +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import RedirectResponse +from fastapi.staticfiles import StaticFiles + from app.api.routers import api_router from app.middlewares.frontend import FrontendProxyMiddleware from app.observability import init_observability from app.settings import init_settings -from fastapi import FastAPI -from fastapi.responses import RedirectResponse -from fastapi.staticfiles import StaticFiles servers = [] app_name = os.getenv("FLY_APP_NAME") @@ -28,6 +31,16 @@ environment = os.getenv("ENVIRONMENT", "dev") # Default to 'development' if not set logger = logging.getLogger("uvicorn") +# Add CORS middleware for development +if environment == "dev": + app.add_middleware( + CORSMiddleware, + allow_origin_regex="http://localhost:\d+|http://0\.0\.0\.0:\d+", + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + def mount_static_files(directory, path, html=False): if os.path.exists(directory): diff --git a/templates/types/streaming/nextjs/app/components/ui/accordion.tsx b/templates/types/streaming/nextjs/app/components/ui/accordion.tsx new file mode 100644 index 000000000..d10ab2763 --- /dev/null +++ b/templates/types/streaming/nextjs/app/components/ui/accordion.tsx @@ -0,0 +1,56 @@ +"use client"; + +import * as AccordionPrimitive from "@radix-ui/react-accordion"; +import { ChevronDown } from "lucide-react"; +import * as React from "react"; +import { cn } from "./lib/utils"; + +const Accordion = AccordionPrimitive.Root; + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AccordionItem.displayName = "AccordionItem"; + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + svg]:rotate-180", + className, + )} + {...props} + > + {children} + + + +)); +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
{children}
+
+)); +AccordionContent.displayName = AccordionPrimitive.Content.displayName; + +export { Accordion, AccordionContent, AccordionItem, AccordionTrigger }; diff --git a/templates/types/streaming/nextjs/app/components/ui/card.tsx b/templates/types/streaming/nextjs/app/components/ui/card.tsx new file mode 100644 index 000000000..da14564a2 --- /dev/null +++ b/templates/types/streaming/nextjs/app/components/ui/card.tsx @@ -0,0 +1,82 @@ +import * as React from "react"; +import { cn } from "./lib/utils"; + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +Card.displayName = "Card"; + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardHeader.displayName = "CardHeader"; + +const CardTitle = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardTitle.displayName = "CardTitle"; + +const CardDescription = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardDescription.displayName = "CardDescription"; + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardContent.displayName = "CardContent"; + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardFooter.displayName = "CardFooter"; + +export { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +}; diff --git a/templates/types/streaming/nextjs/app/components/ui/chat/custom/deep-research-card.tsx b/templates/types/streaming/nextjs/app/components/ui/chat/custom/deep-research-card.tsx index b9c42561e..03d0bd8c1 100644 --- a/templates/types/streaming/nextjs/app/components/ui/chat/custom/deep-research-card.tsx +++ b/templates/types/streaming/nextjs/app/components/ui/chat/custom/deep-research-card.tsx @@ -4,7 +4,6 @@ import { Message } from "@llamaindex/chat-ui"; import { AlertCircle, CheckCircle2, - ChevronDown, CircleDashed, Clock, NotebookPen, @@ -12,10 +11,12 @@ import { } from "lucide-react"; import { useMemo } from "react"; import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, -} from "../../collapsible"; + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "../../accordion"; +import { Card, CardContent, CardHeader, CardTitle } from "../../card"; import { cn } from "../../lib/utils"; import { Markdown } from "./markdown"; @@ -163,63 +164,53 @@ export function DeepResearchCard({ } return ( -
- {state.retrieve.state !== null && ( -
-

+ + + {state.retrieve.state !== null && ( + - - {state.retrieve.state === "inprogress" - ? "Searching..." - : "Search completed"} - -

-
- )} - - {state.analyze.state !== null && ( -
-

+ {state.retrieve.state === "inprogress" + ? "Searching..." + : "Search completed"} + + )} + {state.analyze.state !== null && ( + - - {state.analyze.state === "inprogress" - ? "Analyzing..." - : "Analysis"} - -

- {state.analyze.questions.length > 0 && ( -
- {state.analyze.questions.map((question: QuestionState) => ( - - -
-
- {stateIcon[question.state]} -
- - {question.question} - - + {state.analyze.state === "inprogress" ? "Analyzing..." : "Analysis"} + + )} + + + + {state.analyze.questions.length > 0 && ( + + {state.analyze.questions.map((question: QuestionState) => ( + + +
+
+ {stateIcon[question.state]}
- - {question.answer && ( - -
- -
-
- )} - - ))} -
- )} -
- )} -
+ + {question.question} + +
+ + {question.answer && ( + + + + )} + + ))} + + )} + + ); } diff --git a/templates/types/streaming/nextjs/package.json b/templates/types/streaming/nextjs/package.json index 1887356dc..80612d9e5 100644 --- a/templates/types/streaming/nextjs/package.json +++ b/templates/types/streaming/nextjs/package.json @@ -12,6 +12,7 @@ "dependencies": { "@apidevtools/swagger-parser": "^10.1.0", "@e2b/code-interpreter": "^1.0.4", + "@radix-ui/react-accordion": "^1.2.2", "@radix-ui/react-collapsible": "^1.0.3", "@radix-ui/react-select": "^2.1.1", "@radix-ui/react-slot": "^1.0.2", @@ -19,7 +20,7 @@ "@llamaindex/chat-ui": "0.0.14", "ai": "^4.0.3", "ajv": "^8.12.0", - "class-variance-authority": "^0.7.0", + "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "dotenv": "^16.3.1", "duck-duck-scrape": "^2.2.5", @@ -32,7 +33,7 @@ "react-dom": "^19.0.0", "papaparse": "^5.4.1", "supports-color": "^8.1.1", - "tailwind-merge": "^2.1.0", + "tailwind-merge": "^2.6.0", "tiktoken": "^1.0.15", "uuid": "^9.0.1", "marked": "^14.1.2"