chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,256 @@
|
||||
/**
|
||||
* This is a port of GPT Newspaper to LangGraph JS, adapted from the original Python code.
|
||||
*
|
||||
* https://github.com/assafelovic/gpt-newspaper
|
||||
*/
|
||||
import { HumanMessage, SystemMessage } from "@langchain/core/messages";
|
||||
import { ChatOpenAI } from "@langchain/openai";
|
||||
import { StateGraph, END } from "@langchain/langgraph";
|
||||
import { RunnableLambda } from "@langchain/core/runnables";
|
||||
import { TavilySearchAPIRetriever } from "@langchain/community/retrievers/tavily_search_api";
|
||||
|
||||
interface AgentState {
|
||||
topic: string;
|
||||
searchResults?: string;
|
||||
article?: string;
|
||||
critique?: string;
|
||||
}
|
||||
|
||||
function model() {
|
||||
return new ChatOpenAI({
|
||||
temperature: 0,
|
||||
modelName: "gpt-3.5-turbo-0125",
|
||||
});
|
||||
}
|
||||
|
||||
async function search(state: {
|
||||
agentState: AgentState;
|
||||
}): Promise<{ agentState: AgentState }> {
|
||||
const retriever = new TavilySearchAPIRetriever({
|
||||
k: 10,
|
||||
});
|
||||
let topic = state.agentState.topic;
|
||||
// must be at least 5 characters long
|
||||
if (topic.length < 5) {
|
||||
topic = "topic: " + topic;
|
||||
}
|
||||
console.log("searching for topic:", topic);
|
||||
const docs = await retriever.getRelevantDocuments(topic);
|
||||
console.log("search result length:", docs.length);
|
||||
return {
|
||||
agentState: {
|
||||
...state.agentState,
|
||||
searchResults: JSON.stringify(docs),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function curate(state: {
|
||||
agentState: AgentState;
|
||||
}): Promise<{ agentState: AgentState }> {
|
||||
console.log("curating search results");
|
||||
const response = await model().invoke(
|
||||
[
|
||||
new SystemMessage(
|
||||
`You are a personal newspaper editor.
|
||||
Your sole task is to return a list of URLs of the 5 most relevant articles for the provided topic or query as a JSON list of strings
|
||||
in this format:
|
||||
{
|
||||
urls: ["url1", "url2", "url3", "url4", "url5"]
|
||||
}
|
||||
.`.replace(/\s+/g, " "),
|
||||
),
|
||||
new HumanMessage(
|
||||
`Today's date is ${new Date().toLocaleDateString("en-GB")}.
|
||||
Topic or Query: ${state.agentState.topic}
|
||||
|
||||
Here is a list of articles:
|
||||
${state.agentState.searchResults}`.replace(/\s+/g, " "),
|
||||
),
|
||||
],
|
||||
{
|
||||
response_format: {
|
||||
type: "json_object",
|
||||
},
|
||||
},
|
||||
);
|
||||
const urls = JSON.parse(response.content as string).urls;
|
||||
const searchResults = JSON.parse(state.agentState.searchResults!);
|
||||
const newSearchResults = searchResults.filter((result: any) => {
|
||||
return urls.includes(result.metadata.source);
|
||||
});
|
||||
console.log("curated search results:", newSearchResults);
|
||||
return {
|
||||
agentState: {
|
||||
...state.agentState,
|
||||
searchResults: JSON.stringify(newSearchResults),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function critique(state: {
|
||||
agentState: AgentState;
|
||||
}): Promise<{ agentState: AgentState }> {
|
||||
console.log("critiquing article");
|
||||
let feedbackInstructions = "";
|
||||
if (state.agentState.critique) {
|
||||
feedbackInstructions =
|
||||
`The writer has revised the article based on your previous critique: ${state.agentState.critique}
|
||||
The writer might have left feedback for you encoded between <FEEDBACK> tags.
|
||||
The feedback is only for you to see and will be removed from the final article.
|
||||
`.replace(/\s+/g, " ");
|
||||
}
|
||||
const response = await model().invoke([
|
||||
new SystemMessage(
|
||||
`You are a personal newspaper writing critique. Your sole purpose is to provide short feedback on a written
|
||||
article so the writer will know what to fix.
|
||||
Today's date is ${new Date().toLocaleDateString("en-GB")}
|
||||
Your task is to provide a really short feedback on the article only if necessary.
|
||||
if you think the article is good, please return [DONE].
|
||||
you can provide feedback on the revised article or just
|
||||
return [DONE] if you think the article is good.
|
||||
Please return a string of your critique or [DONE].`.replace(/\s+/g, " "),
|
||||
),
|
||||
new HumanMessage(
|
||||
`${feedbackInstructions}
|
||||
This is the article: ${state.agentState.article}`,
|
||||
),
|
||||
]);
|
||||
const content = response.content as string;
|
||||
console.log("critique:", content);
|
||||
return {
|
||||
agentState: {
|
||||
...state.agentState,
|
||||
critique: content.includes("[DONE]") ? undefined : content,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function write(state: {
|
||||
agentState: AgentState;
|
||||
}): Promise<{ agentState: AgentState }> {
|
||||
console.log("writing article");
|
||||
const response = await model().invoke([
|
||||
new SystemMessage(
|
||||
`You are a personal newspaper writer. Your sole purpose is to write a well-written article about a
|
||||
topic using a list of articles. Write 5 paragraphs in markdown.`.replace(
|
||||
/\s+/g,
|
||||
" ",
|
||||
),
|
||||
),
|
||||
new HumanMessage(
|
||||
`Today's date is ${new Date().toLocaleDateString("en-GB")}.
|
||||
Your task is to write a critically acclaimed article for me about the provided query or
|
||||
topic based on the sources.
|
||||
Here is a list of articles: ${state.agentState.searchResults}
|
||||
This is the topic: ${state.agentState.topic}
|
||||
Please return a well-written article based on the provided information.`.replace(
|
||||
/\s+/g,
|
||||
" ",
|
||||
),
|
||||
),
|
||||
]);
|
||||
const content = response.content as string;
|
||||
console.log("article:", content);
|
||||
return {
|
||||
agentState: {
|
||||
...state.agentState,
|
||||
article: content,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function revise(state: {
|
||||
agentState: AgentState;
|
||||
}): Promise<{ agentState: AgentState }> {
|
||||
console.log("revising article");
|
||||
const response = await model().invoke([
|
||||
new SystemMessage(
|
||||
`You are a personal newspaper editor. Your sole purpose is to edit a well-written article about a
|
||||
topic based on given critique.`.replace(/\s+/g, " "),
|
||||
),
|
||||
new HumanMessage(
|
||||
`Your task is to edit the article based on the critique given.
|
||||
This is the article: ${state.agentState.article}
|
||||
This is the critique: ${state.agentState.critique}
|
||||
Please return the edited article based on the critique given.
|
||||
You may leave feedback about the critique encoded between <FEEDBACK> tags like this:
|
||||
<FEEDBACK> here goes the feedback ...</FEEDBACK>`.replace(/\s+/g, " "),
|
||||
),
|
||||
]);
|
||||
const content = response.content as string;
|
||||
console.log("revised article:", content);
|
||||
return {
|
||||
agentState: {
|
||||
...state.agentState,
|
||||
article: content,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const agentState = {
|
||||
agentState: {
|
||||
value: (x: AgentState, y: AgentState) => y,
|
||||
default: () => ({
|
||||
topic: "",
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
// Define the function that determines whether to continue or not
|
||||
const shouldContinue = (state: { agentState: AgentState }) => {
|
||||
const result = state.agentState.critique === undefined ? "end" : "continue";
|
||||
return result;
|
||||
};
|
||||
|
||||
const workflow = new StateGraph({
|
||||
channels: agentState,
|
||||
});
|
||||
|
||||
workflow.addNode("search", new RunnableLambda({ func: search }) as any);
|
||||
workflow.addNode("curate", new RunnableLambda({ func: curate }) as any);
|
||||
workflow.addNode("write", new RunnableLambda({ func: write }) as any);
|
||||
workflow.addNode("critique", new RunnableLambda({ func: critique }) as any);
|
||||
workflow.addNode("revise", new RunnableLambda({ func: revise }) as any);
|
||||
|
||||
workflow.addEdge("search", "curate");
|
||||
workflow.addEdge("curate", "write");
|
||||
workflow.addEdge("write", "critique");
|
||||
|
||||
// We now add a conditional edge
|
||||
workflow.addConditionalEdges(
|
||||
// First, we define the start node. We use `agent`.
|
||||
// This means these are the edges taken after the `agent` node is called.
|
||||
"critique",
|
||||
// Next, we pass in the function that will determine which node is called next.
|
||||
shouldContinue,
|
||||
// Finally we pass in a mapping.
|
||||
// The keys are strings, and the values are other nodes.
|
||||
// END is a special node marking that the graph should finish.
|
||||
// What will happen is we will call `should_continue`, and then the output of that
|
||||
// will be matched against the keys in this mapping.
|
||||
// Based on which one it matches, that node will then be called.
|
||||
{
|
||||
// If `tools`, then we call the tool node.
|
||||
continue: "revise",
|
||||
// Otherwise we finish.
|
||||
end: END,
|
||||
},
|
||||
);
|
||||
|
||||
workflow.addEdge("revise", "critique");
|
||||
|
||||
workflow.setEntryPoint("search");
|
||||
const app = workflow.compile();
|
||||
|
||||
export async function researchWithLangGraph(topic: string) {
|
||||
const inputs = {
|
||||
agentState: {
|
||||
topic,
|
||||
},
|
||||
};
|
||||
const result = await app.invoke(inputs);
|
||||
const regex = /<FEEDBACK>[\s\S]*?<\/FEEDBACK>/g;
|
||||
const article = result.agentState.article.replace(regex, "");
|
||||
return article;
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import { researchWithLangGraph } from "./research";
|
||||
import { Action } from "@copilotkit/shared";
|
||||
import { NextRequest } from "next/server";
|
||||
import {
|
||||
CopilotRuntime,
|
||||
copilotRuntimeNextJSAppRouterEndpoint,
|
||||
OpenAIAdapter,
|
||||
} from "@copilotkit/runtime";
|
||||
|
||||
const UNSPLASH_ACCESS_KEY_ENV = "UNSPLASH_ACCESS_KEY";
|
||||
const UNSPLASH_ACCESS_KEY = process.env[UNSPLASH_ACCESS_KEY_ENV];
|
||||
|
||||
const researchAction: Action<any> = {
|
||||
name: "research",
|
||||
description:
|
||||
"Call this function to conduct research on a certain topic. Respect other notes about when to call this function",
|
||||
parameters: [
|
||||
{
|
||||
name: "topic",
|
||||
type: "string",
|
||||
description: "The topic to research. 5 characters or longer.",
|
||||
},
|
||||
],
|
||||
handler: async ({ topic }) => {
|
||||
console.log("Researching topic: ", topic);
|
||||
return await researchWithLangGraph(topic);
|
||||
},
|
||||
};
|
||||
|
||||
export const POST = async (req: NextRequest) => {
|
||||
const actions: Action<any>[] = [
|
||||
{
|
||||
name: "getImageUrl",
|
||||
description: "Get an image url for a topic",
|
||||
parameters: [
|
||||
{
|
||||
name: "topic",
|
||||
description: "The topic of the image",
|
||||
},
|
||||
],
|
||||
handler: async ({ topic }) => {
|
||||
if (UNSPLASH_ACCESS_KEY) {
|
||||
const response = await fetch(
|
||||
`https://api.unsplash.com/search/photos?query=${encodeURIComponent(
|
||||
topic,
|
||||
)}&per_page=10&order_by=relevant&content_filter=high`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Client-ID ${UNSPLASH_ACCESS_KEY}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
const data = await response.json();
|
||||
if (data.results && data.results.length > 0) {
|
||||
const randomIndex = Math.floor(Math.random() * data.results.length);
|
||||
return data.results[randomIndex].urls.regular;
|
||||
}
|
||||
}
|
||||
return (
|
||||
'url("https://loremflickr.com/800/600/' +
|
||||
encodeURIComponent(topic) +
|
||||
'")'
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
if (
|
||||
process.env["TAVILY_API_KEY"] &&
|
||||
process.env["TAVILY_API_KEY"] !== "NONE"
|
||||
) {
|
||||
actions.push(researchAction);
|
||||
}
|
||||
|
||||
const openaiModel = process.env["OPENAI_MODEL"];
|
||||
|
||||
console.log("ENV.COPILOT_CLOUD_API_KEY", process.env.COPILOT_CLOUD_API_KEY);
|
||||
|
||||
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
|
||||
runtime: new CopilotRuntime({ actions }),
|
||||
serviceAdapter: new OpenAIAdapter({ model: openaiModel }),
|
||||
endpoint: req.nextUrl.pathname,
|
||||
});
|
||||
|
||||
return handleRequest(req);
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { OpenAI } from "openai";
|
||||
|
||||
// export const runtime = "edge";
|
||||
|
||||
const openai = new OpenAI();
|
||||
|
||||
export async function POST(req: Request): Promise<Response> {
|
||||
try {
|
||||
const formData = await req.formData();
|
||||
const file = formData.get("file") as File;
|
||||
|
||||
if (!file) {
|
||||
return new Response("File not provided", { status: 400 });
|
||||
}
|
||||
|
||||
const transcription = await openai.audio.transcriptions.create({
|
||||
file,
|
||||
model: "whisper-1",
|
||||
});
|
||||
|
||||
return new Response(JSON.stringify(transcription), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
} catch (error: any) {
|
||||
return new Response(JSON.stringify({ error: error.message }), {
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { OpenAI } from "openai";
|
||||
|
||||
export const runtime = "edge";
|
||||
|
||||
export async function GET(req: Request): Promise<Response> {
|
||||
const openai = new OpenAI();
|
||||
|
||||
const url = new URL(req.url);
|
||||
const text = url.searchParams.get("text"); // 'text' is the query parameter name
|
||||
|
||||
if (!text) {
|
||||
return new Response("Text parameter is missing", { status: 400 });
|
||||
}
|
||||
|
||||
const response = await openai.audio.speech.create({
|
||||
voice: "alloy",
|
||||
input: text,
|
||||
model: "tts-1",
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
Reference in New Issue
Block a user