@nxcode/sdk 1.0.5 → 1.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -465,9 +465,8 @@ class NxcodeAI {
465
465
  */
466
466
  async chat(options) {
467
467
  const token = this.getToken();
468
- if (!token) {
469
- throw new Error('Not authenticated. Please login first.');
470
- }
468
+ // Allow anonymous requests for dev preview and apps with anonymous AI enabled
469
+ const headers = token ? this.getHeaders(token) : this.getAnonymousHeaders();
471
470
  const model = options.model || 'fast';
472
471
  // Build Gemini API request
473
472
  const geminiRequest = {
@@ -482,7 +481,7 @@ class NxcodeAI {
482
481
  }
483
482
  const response = await fetch(`${this.apiEndpoint}/api/ai-gateway/v1beta/models/${model}:generateContent`, {
484
483
  method: 'POST',
485
- headers: this.getHeaders(token),
484
+ headers,
486
485
  body: JSON.stringify(geminiRequest),
487
486
  });
488
487
  if (!response.ok) {
@@ -689,24 +688,286 @@ class NxcodeAI {
689
688
  };
690
689
  }
691
690
  async handleError(response) {
692
- if (response.status === 401) {
693
- throw new Error('Session expired. Please login again.');
694
- }
695
- if (response.status === 402) {
696
- throw new Error('Insufficient balance. Please top up to continue.');
697
- }
698
691
  let message = 'AI request failed';
699
692
  try {
700
693
  const data = await response.json();
701
694
  message = data.detail || data.message || message;
702
695
  }
703
696
  catch {
704
- // Ignore parse errors
697
+ // Ignore parse errors, use default message based on status
698
+ if (response.status === 401) {
699
+ message = 'Session expired. Please login again.';
700
+ }
701
+ else if (response.status === 402) {
702
+ message = 'Insufficient balance. Please top up to continue.';
703
+ }
705
704
  }
706
705
  throw new Error(message);
707
706
  }
708
707
  }
709
708
 
709
+ /**
710
+ * Nxcode SDK - Agent Module
711
+ *
712
+ * Provides AI agents with tool calling capabilities.
713
+ * Agents can call backend endpoints as tools and maintain conversation history.
714
+ */
715
+ // ==================== Helper Functions ====================
716
+ /**
717
+ * Convert simple schema to JSON Schema format for Gemini
718
+ */
719
+ function toJsonSchemaProperty(schema) {
720
+ if (schema === 'string')
721
+ return { type: 'STRING' };
722
+ if (schema === 'number')
723
+ return { type: 'NUMBER' };
724
+ if (schema === 'boolean')
725
+ return { type: 'BOOLEAN' };
726
+ if (Array.isArray(schema)) {
727
+ return {
728
+ type: 'ARRAY',
729
+ items: toJsonSchemaProperty(schema[0]),
730
+ };
731
+ }
732
+ if (typeof schema === 'object') {
733
+ const properties = {};
734
+ const required = [];
735
+ for (const [key, value] of Object.entries(schema)) {
736
+ properties[key] = toJsonSchemaProperty(value);
737
+ required.push(key);
738
+ }
739
+ return { type: 'OBJECT', properties, required };
740
+ }
741
+ return { type: 'STRING' };
742
+ }
743
+ /**
744
+ * Convert AgentTool to Gemini function declaration
745
+ */
746
+ function toolToGeminiFunction(tool) {
747
+ const declaration = {
748
+ name: tool.name,
749
+ description: tool.description,
750
+ };
751
+ // Always include parameters object (even if empty) for Gemini compatibility
752
+ const properties = {};
753
+ const required = [];
754
+ if (tool.parameters) {
755
+ for (const [key, value] of Object.entries(tool.parameters)) {
756
+ properties[key] = toJsonSchemaProperty(value);
757
+ required.push(key);
758
+ }
759
+ }
760
+ declaration.parameters = {
761
+ type: 'OBJECT',
762
+ properties,
763
+ required,
764
+ };
765
+ return declaration;
766
+ }
767
+ // ==================== Agent Class ====================
768
+ class Agent {
769
+ constructor(options, apiEndpoint, appId, getToken) {
770
+ this.history = [];
771
+ this.options = {
772
+ instructions: options.instructions,
773
+ tools: options.tools,
774
+ model: options.model || 'fast',
775
+ maxSteps: options.maxSteps || 10,
776
+ };
777
+ this.apiEndpoint = apiEndpoint;
778
+ this.appId = appId;
779
+ this.getToken = getToken;
780
+ }
781
+ /**
782
+ * Run the agent with user input
783
+ */
784
+ async run(input, callbacks) {
785
+ const token = this.getToken();
786
+ if (!token) {
787
+ throw new Error('Not authenticated. Please login first.');
788
+ }
789
+ // Add user message to history
790
+ this.history.push({ role: 'user', content: input });
791
+ const toolCalls = [];
792
+ let totalInputTokens = 0;
793
+ let totalOutputTokens = 0;
794
+ let steps = 0;
795
+ // Agent loop
796
+ while (steps < this.options.maxSteps) {
797
+ steps++;
798
+ // Call Gemini with tools
799
+ const response = await this.callGemini(token);
800
+ totalInputTokens += response.usage?.inputTokens || 0;
801
+ totalOutputTokens += response.usage?.outputTokens || 0;
802
+ // Check if AI wants to call a function
803
+ const functionCall = response.functionCall;
804
+ if (functionCall) {
805
+ callbacks?.onToolCall?.(functionCall.name, functionCall.args);
806
+ // Find the tool
807
+ const tool = this.options.tools.find((t) => t.name === functionCall.name);
808
+ if (!tool) {
809
+ throw new Error(`Unknown tool: ${functionCall.name}`);
810
+ }
811
+ // Call the tool endpoint
812
+ const result = await this.callTool(tool, functionCall.args, token);
813
+ callbacks?.onToolResult?.(functionCall.name, result);
814
+ // Record tool call
815
+ toolCalls.push({
816
+ name: functionCall.name,
817
+ params: functionCall.args,
818
+ result,
819
+ });
820
+ // Add function call and response to history using raw parts from Gemini
821
+ this.history.push({
822
+ role: 'model',
823
+ content: '',
824
+ parts: response.rawParts || [{ functionCall: { name: functionCall.name, args: functionCall.args } }],
825
+ });
826
+ this.history.push({
827
+ role: 'user',
828
+ content: '',
829
+ parts: [{ functionResponse: { name: functionCall.name, response: result } }],
830
+ });
831
+ // Continue loop to let AI process the result
832
+ continue;
833
+ }
834
+ // No function call - AI is done, return the text response
835
+ const output = response.text || '';
836
+ // Add assistant response to history
837
+ this.history.push({ role: 'assistant', content: output });
838
+ return {
839
+ output,
840
+ toolCalls,
841
+ usage: {
842
+ inputTokens: totalInputTokens,
843
+ outputTokens: totalOutputTokens,
844
+ },
845
+ };
846
+ }
847
+ throw new Error(`Agent exceeded maximum steps (${this.options.maxSteps})`);
848
+ }
849
+ /**
850
+ * Clear conversation history
851
+ */
852
+ reset() {
853
+ this.history = [];
854
+ }
855
+ /**
856
+ * Get current conversation history
857
+ */
858
+ getHistory() {
859
+ return [...this.history];
860
+ }
861
+ // ==================== Private Methods ====================
862
+ async callGemini(token) {
863
+ // Build contents from history
864
+ const contents = this.history.map((msg) => {
865
+ if (msg.parts) {
866
+ return {
867
+ role: msg.role === 'assistant' ? 'model' : msg.role,
868
+ parts: msg.parts,
869
+ };
870
+ }
871
+ return {
872
+ role: msg.role === 'assistant' ? 'model' : 'user',
873
+ parts: [{ text: msg.content }],
874
+ };
875
+ });
876
+ // Build tools
877
+ const tools = [
878
+ {
879
+ functionDeclarations: this.options.tools.map(toolToGeminiFunction),
880
+ },
881
+ ];
882
+ // Build request with tool config to encourage tool use
883
+ const request = {
884
+ contents,
885
+ tools,
886
+ toolConfig: {
887
+ functionCallingConfig: {
888
+ mode: 'AUTO', // Let model decide when to use tools
889
+ },
890
+ },
891
+ systemInstruction: {
892
+ parts: [{ text: this.options.instructions }],
893
+ },
894
+ };
895
+ const response = await fetch(`${this.apiEndpoint}/api/ai-gateway/v1beta/models/${this.options.model}:generateContent`, {
896
+ method: 'POST',
897
+ headers: {
898
+ 'Content-Type': 'application/json',
899
+ 'X-App-Id': this.appId,
900
+ Authorization: `Bearer ${token}`,
901
+ },
902
+ body: JSON.stringify(request),
903
+ });
904
+ if (!response.ok) {
905
+ const error = await response.json().catch(() => ({}));
906
+ throw new Error(error.detail || error.message || 'Agent API call failed');
907
+ }
908
+ const data = await response.json();
909
+ // Parse response
910
+ const candidate = data.candidates?.[0];
911
+ const parts = candidate?.content?.parts || [];
912
+ // Check for function call (support both camelCase and snake_case)
913
+ for (const part of parts) {
914
+ const funcCall = part.functionCall || part.function_call;
915
+ if (funcCall) {
916
+ return {
917
+ functionCall: {
918
+ name: funcCall.name,
919
+ args: funcCall.args || {},
920
+ },
921
+ rawParts: parts, // Save raw parts including thought_signature
922
+ usage: {
923
+ inputTokens: data.usageMetadata?.promptTokenCount || 0,
924
+ outputTokens: data.usageMetadata?.candidatesTokenCount || 0,
925
+ },
926
+ };
927
+ }
928
+ }
929
+ // Return text response
930
+ const text = parts.find((p) => p.text)?.text || '';
931
+ return {
932
+ text,
933
+ usage: {
934
+ inputTokens: data.usageMetadata?.promptTokenCount || 0,
935
+ outputTokens: data.usageMetadata?.candidatesTokenCount || 0,
936
+ },
937
+ };
938
+ }
939
+ async callTool(tool, params, token) {
940
+ // Build context to pass to tool
941
+ const body = {
942
+ ...params,
943
+ _context: {
944
+ agentId: this.options.instructions.slice(0, 20), // Simple ID
945
+ stepIndex: this.history.length,
946
+ previousTools: this.history
947
+ .filter((m) => m.parts?.some((p) => p.functionResponse))
948
+ .map((m) => {
949
+ const fr = m.parts?.find((p) => p.functionResponse)?.functionResponse;
950
+ return fr ? { name: fr.name, result: fr.response } : null;
951
+ })
952
+ .filter(Boolean),
953
+ },
954
+ };
955
+ const response = await fetch(tool.endpoint, {
956
+ method: 'POST',
957
+ headers: {
958
+ 'Content-Type': 'application/json',
959
+ Authorization: `Bearer ${token}`,
960
+ },
961
+ body: JSON.stringify(body),
962
+ });
963
+ if (!response.ok) {
964
+ const error = await response.text();
965
+ return { error: `Tool call failed: ${error}` };
966
+ }
967
+ return response.json();
968
+ }
969
+ }
970
+
710
971
  /**
711
972
  * Nxcode SDK
712
973
  *
@@ -971,6 +1232,37 @@ class NxcodeSDK {
971
1232
  await self.ensureInitialized();
972
1233
  return self._ai.generateStream(options);
973
1234
  },
1235
+ /**
1236
+ * Create an AI agent with tool calling capabilities
1237
+ *
1238
+ * @example
1239
+ * const agent = Nxcode.ai.createAgent({
1240
+ * instructions: 'You are a helpful assistant that can search and send emails.',
1241
+ * tools: [
1242
+ * {
1243
+ * name: 'search_emails',
1244
+ * description: 'Search user emails',
1245
+ * parameters: { query: 'string' },
1246
+ * endpoint: '/api/emails/search'
1247
+ * },
1248
+ * {
1249
+ * name: 'send_email',
1250
+ * description: 'Send an email',
1251
+ * parameters: { to: 'string', subject: 'string', body: 'string' },
1252
+ * endpoint: '/api/emails/send'
1253
+ * }
1254
+ * ]
1255
+ * });
1256
+ *
1257
+ * const result = await agent.run('Find emails from John and reply to him');
1258
+ * console.log(result.output);
1259
+ */
1260
+ createAgent(options) {
1261
+ if (!self.config) {
1262
+ throw new Error('Nxcode SDK not configured. Call Nxcode.configure(appId) first.');
1263
+ }
1264
+ return new Agent(options, self.apiEndpoint, self.config.appId, () => self._auth?.getToken() || null);
1265
+ },
974
1266
  };
975
1267
  }
976
1268
  // ==================== Utility Methods ====================
@@ -1000,5 +1292,5 @@ if (typeof window !== 'undefined') {
1000
1292
  window.Nxcode = Nxcode;
1001
1293
  }
1002
1294
 
1003
- export { Nxcode, Nxcode as default };
1295
+ export { Agent, Nxcode, Nxcode as default };
1004
1296
  //# sourceMappingURL=nxcode.esm.js.map