diff --git a/indexers/agents/agents.js b/indexers/agents/agents.js new file mode 100644 index 00000000..258d97ea --- /dev/null +++ b/indexers/agents/agents.js @@ -0,0 +1,122 @@ +import { Block } from "@near-lake/primitives"; +/** + * Note: We only support javascript at the moment. We will support Rust, Typescript in a further release. + */ + +/** + * getBlock(block, context) applies your custom logic to a Block on Near and commits the data to a database. + * context is a global variable that contains helper methods. + * context.db is a subfield which contains helper methods to interact with your database. + * + * Learn more about indexers here: https://docs.near.org/concepts/advanced/indexers + * + * @param {block} Block - A Near Protocol Block + */ +async function getBlock(block: Block) { + function base64decode(encodedValue) { + try { + const buff = Buffer.from(encodedValue, "base64"); + const str = buff.toString("utf-8").replace(/\\xa0/g, " "); + return JSON.parse(str); + } catch (error) { + console.log( + 'Error parsing JSON - skipping data for "functionCallOperation.args"', + error + ); + } + } + + const getFunctionCallsFunction = (block, contract, method) => { + return block + .actions() + .filter((action) => action.receiverId === contract) + .flatMap((action) => + action.operations + .map((operation) => operation["FunctionCall"]) + .filter((operation) => operation?.methodName === method) + .map((functionCallOperation) => ({ + ...functionCallOperation, + args: base64decode(functionCallOperation.args), + receiptId: action.receiptId, + })) + .filter((a) => + block + .receipts() + .find((r) => r.receiptId === a.receiptId) + .status.hasOwnProperty("SuccessValue") + ) + ); + }; + + const getSocialOperations = (block, operation) => { + const contract = "social.near"; + const method = "set"; + return getFunctionCallsFunction(block, contract, method) + .filter((functionCall) => { + if ( + !functionCall || + !functionCall.args || + !functionCall.args.data || + !Object.keys(functionCall.args.data) || + !Object.keys(functionCall.args.data)[0] + ) { + console.log("Set operation did not have arg data in expected format"); + return; + } + const accountId = Object.keys(functionCall.args.data)[0]; + const accountData = functionCall.args.data[accountId]; + if (!accountData) { + console.log( + "Set operation did not have arg data for accountId in expected format" + ); + return; + } + return accountData[operation]; + }) + .map((functionCall) => { + const accountId = Object.keys(functionCall.args.data)[0]; + return { + accountId, + data: functionCall.args.data[accountId][operation], + }; + }); + }; + + const agentWrites = getSocialOperations(block, "agent"); + agentWrites.map(async ({ accountId, data }) => { + try { + const agent = { + name: data.name, + account_id: accountId, + display_name: data.displayName, + preferred_provider: data.preferredProvider, + preferred_model: data.preferredModel, + prompt: data.prompt, + variables: data.variables, + component: data.component, + logo_url: data.logoUrl, + tools: data.tools, + properties: data.properties, + }; + await context.db.Agents.upsert( + agent, + ["account_id", "name"], + [ + "display_name", + "preferred_provider", + "preferred_model", + "prompt", + "variables", + "component", + "logo_url", + "tools", + "properties", + ] + ); + + console.log(`Agent from ${accountId} has been added to the database`); + } catch (e) { + console.log(`Failed to store agent from ${accountId} to the database`, e); + } + }); +} diff --git a/indexers/agents/agents.sql b/indexers/agents/agents.sql new file mode 100644 index 00000000..14cdcea1 --- /dev/null +++ b/indexers/agents/agents.sql @@ -0,0 +1,20 @@ +CREATE TABLE + "agents" ( + "id" SERIAL NOT NULL, + "account_id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "display_name" TEXT, + "preferred_provider" TEXT, + "preferred_model" TEXT, + "input_adaptor" TEXT, + "output_adaptor" TEXT, + "prompt" TEXT, + "variables" TEXT, + "component" TEXT, + "logo_url" TEXT, + "tools" JSONB, + "properties" JSONB, + PRIMARY KEY ("id") +); + +ALTER TABLE "agents" ADD CONSTRAINT "unique_name" UNIQUE ("account_id", "name"); \ No newline at end of file diff --git a/src/Entities/AgentFramework/AgentCard.jsx b/src/Entities/AgentFramework/AgentCard.jsx new file mode 100644 index 00000000..2be95282 --- /dev/null +++ b/src/Entities/AgentFramework/AgentCard.jsx @@ -0,0 +1,98 @@ +const { href } = VM.require("devhub.near/widget/core.lib.url"); + +if (!href) { + return <>; +} + +const Card = styled.div` + cursor: pointer; + background-color: white; + border-radius: 0.5rem; + padding: 1.5rem; + gap: 1rem; + height: 100%; + min-height: 12rem; + + transition: all 300ms; + box-shadow: + 0 1px 3px 0 rgb(0 0 0 / 0.1), + 0 1px 2px -1px rgb(0 0 0 / 0.1); + + &:hover { + box-shadow: + 0 4px 6px -1px rgb(0 0 0 / 0.1), + 0 2px 4px -2px rgb(0 0 0 / 0.1); + } + + img.logo { + height: 6rem; + width: 6rem; + border-radius: 50%; + + object-fit: cover; + } + + h3, + p { + margin: 0; + } + + h3 { + font-size: 1.25rem; + font-weight: 600; + } + + p { + font-size: 1rem; + font-weight: 400; + } +`; +const CardBody = styled.div` + display: flex; + align-items: center; +`; +const Break = styled.div` + flex-basis: 100%; + height: 0; +`; +const Actions = styled.div` + padding-top: 16px; + display: flex; + align-items: center; + gap: 12px; +`; +const AgentCard = ({ item }) => { + const { accountId, name, displayName, prompt, logoUrl } = item; + const imageUrl = + logoUrl ?? "https://ipfs.near.social/ipfs/bafkreibysr2mkwhb4j36h2t7mqwhynqdy4vzjfygfkfg65kuspd2bawauu"; + const link = href({ + widgetSrc: `${REPL_ACCOUNT}/widget/Entities.AgentFramework.AgentChat`, + params: { src: `${accountId}/agent/${name}` }, + }); + + return ( + + +
+
+ agent logo +
+ +
+

{displayName}

+

by {accountId}

+ {prompt}, + trigger:

{prompt ? prompt.substring(0, 20) : ""}...

, + }} + /> +
+
+ +
+ ); +}; + +return AgentCard(props); diff --git a/src/Entities/AgentFramework/AgentChat.jsx b/src/Entities/AgentFramework/AgentChat.jsx new file mode 100644 index 00000000..916bca79 --- /dev/null +++ b/src/Entities/AgentFramework/AgentChat.jsx @@ -0,0 +1,107 @@ +const [accountId, agentType, agentName] = props.src.split("/") ?? [null, null, null]; +const blockHeight = props.blockHeight ?? "final"; + +const data = Social.getr(`${accountId}/agent/${agentName}`, blockHeight); + +if (!data) return "Loading..."; + +const [question, setQuestion] = useState(""); +const [prompt, setPrompt] = useState(""); +const [loading, setLoading] = useState(false); +const [response, setResponse] = useState(""); +const [messages, setMessages] = useState([]); + +useEffect(() => { + if (messages.length === 0 || messages[messages.length - 1].role !== "user") { + return; + } + console.log(messages); + setLoading(true); + asyncFetch(`https://ai.near.social/api`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + responseType: "json", + body: JSON.stringify([{ role: "system", content: data.prompt }, ...messages.slice(-1)]), + }) + .then(({ body }) => { + setMessages([...messages, { role: "system", content: body.response }]); + }) + .finally(() => { + setLoading(false); + }); +}, [messages]); + +const submitQuestion = () => { + setMessages([...messages, { role: "user", content: question }]); + setQuestion(""); +}; + +const Wrapper = styled.div` + display: flex; + flex-direction: column; + gap: 48px; +`; + +return ( + +
+
Name: {data.displayName}
+
+ Prompt:
{data.prompt}
+
+ +
+
+ setQuestion(e.target.value)} + onKeyPress={(e) => { + if (e.key === "Enter") { + submitQuestion(); + } + }} + placeholder="What's your question?" + autoFocus + /> + +
+
+
+ {messages.map(({ role, content }, i) => { + return ( +
+ {role === "user" && ( + + )} + +
+ ); + })} + {loading && ( +
+
+
+
+ )} +
+
+
+); diff --git a/src/Entities/AgentFramework/AgentCreate.jsx b/src/Entities/AgentFramework/AgentCreate.jsx new file mode 100644 index 00000000..f2397be4 --- /dev/null +++ b/src/Entities/AgentFramework/AgentCreate.jsx @@ -0,0 +1,154 @@ +const { typeMatch } = VM.require("devhub.near/widget/core.lib.struct"); + +if (!typeMatch) { + return

Loading modules...

; +} + +const { data, onSubmit, onCancel } = props; + +const onSubmitDefault = (formValues) => { + const { name, ...rest } = formValues; + const agent = { [name]: rest }; + Social.set({ agent: agent }); +}; +const onSubmitFunction = onSubmit ?? onSubmitDefault; + +const AgentInputsPartialSchema = { + name: { + inputProps: { + min: 2, + max: 80, + allowCommaAndSpace: false, + placeholder: "Choose a unique identifier for your agent. Example: travel-agent.", + required: true, + }, + + label: "Name", + order: 1, + }, + displayName: { + inputProps: { + min: 2, + max: 255, + placeholder: "The name that will be displayed to users.", + required: true, + }, + label: "Display Name", + order: 2, + }, + prompt: { + inputProps: { + min: 2, + max: 8192, + placeholder: "The prompt for the agent.", + required: true, + }, + label: "Prompt", + order: 3, + }, + // preferredProvider: { + // inputProps: { + // min: 2, + // max: 255, + // placeholder: "The preferred provider for the agent.", + // required: false, + // }, + // label: "Preferred Provider", + // order: 4, + // }, + // preferredModel: { + // inputProps: { + // min: 2, + // max: 255, + // placeholder: "The preferred model for the agent.", + // required: false, + // }, + // label: "Preferred Model", + // order: 5, + // }, + // variables: { + // inputProps: { + // min: 2, + // max: 255, + // placeholder: "The variables for the agent.", + // required: false, + // }, + // label: "A comma separated list of variables that should be passed into the prompt. Example: ['rfp', 'proposal'].", + // order: 6, + // }, + // component: { + // inputProps: { + // min: 2, + // max: 255, + // placeholder: "The component used to run the agent for the agent.", + // required: false, + // }, + // label: "Component", + // order: 7, + // }, + logoUrl: { + inputProps: { + min: 4, + max: 255, + placeholder: "The logo URL for the agent.", + required: false, + }, + + label: "Logo URL", + order: 8, + }, + // tools: { + // inputProps: { + // min: 2, + // max: 255, + // placeholder: "A JSON array of Tools or Functions the agent should have access to.", + // required: false, + // }, + // + // label: "Tools", + // order: 9, + // }, + // properties: { + // inputProps: { + // min: 2, + // max: 1024, + // placeholder: "JSON properties for the agent.", + // required: false, + // }, + // + // label: "Properties", + // order: 10, + // }, +}; + +const agentInputsValidator = (formValues) => + typeMatch(formValues) && Object.values(formValues).every((value) => typeof value === "string" && value.length > 0); + +const AgentInputsDefaults = { + handle: "", + name: "", + tag: "", + description: "", +}; + +return ( + +); diff --git a/src/Entities/AgentFramework/AgentHeader.jsx b/src/Entities/AgentFramework/AgentHeader.jsx new file mode 100644 index 00000000..b2993662 --- /dev/null +++ b/src/Entities/AgentFramework/AgentHeader.jsx @@ -0,0 +1,426 @@ +const text = props.text; + +let src = ` +
+
+ + + + + + `; +// Adapted from https://codepen.io/sanprieto/details/XWNjBdb under MIT LICENSE + +src = src.replace("TEXT_PROPERTY", text); + +return ( +
+