Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 41 additions & 2 deletions __tests__/feeds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -671,8 +671,8 @@ describe('query feed', () => {
};

const QUERY = `
query Feed($ranking: Ranking, $first: Int, $after: String, $version: Int, $unreadOnly: Boolean, $supportedTypes: [String!]) {
feed(ranking: $ranking, first: $first, after: $after, version: $version, unreadOnly: $unreadOnly, supportedTypes: $supportedTypes) {
query Feed($ranking: Ranking, $first: Int, $after: String, $version: Int, $unreadOnly: Boolean, $supportedTypes: [String!], $noAi: Boolean) {
feed(ranking: $ranking, first: $first, after: $after, version: $version, unreadOnly: $unreadOnly, supportedTypes: $supportedTypes, noAi: $noAi) {
${feedFields()}
}
}
Expand All @@ -689,6 +689,45 @@ describe('query feed', () => {
expect(res.data).toMatchSnapshot();
});

it('should include no-ai blocked tags and title words in the v2 feed config', async () => {
loggedUser = '1';
await saveFeedFixtures();
nock('http://localhost:6002')
.post('/config')
.reply(200, {
user_id: '1',
config: {
providers: {},
},
});
nock('http://localhost:6000')
.post('/feed.json', (body) => {
expect(body.blocked_tags).toEqual(
expect.arrayContaining(['golang', 'ai', 'openai']),
);
expect(body.blocked_title_words).toEqual(
expect.arrayContaining(['Claude', 'Elon Musk']),
);

return true;
})
.reply(200, {
data: [{ post_id: 'p1' }, { post_id: 'p4' }],
cursor: 'b',
});

const res = await client.query(QUERY, {
variables: {
...variables,
noAi: true,
version: 20,
},
});

expect(res.errors).toBeFalsy();
expect(res.data.feed.edges.length).toEqual(2);
});

describe('youtube content', () => {
beforeEach(async () => {
await saveFixtures(con, YouTubePost, videoPostsFixture);
Expand Down
1 change: 1 addition & 0 deletions src/common/feedGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -705,6 +705,7 @@ export const anonymousFeedBuilder = (
whereNotTags(filters.blockedTags!, builder, alias),
);
}

return newBuilder;
};

Expand Down
169 changes: 169 additions & 0 deletions src/common/noAiFilter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
export const NO_AI_BLOCKED_TAGS = [
'ai',
'machine-learning',
'data-science',
'llm',
'deep-learning',
'nlp',
'genai',
'openai',
'chatgpt',
'computer-vision',
'ai-agents',
'neural-networks',
'pytorch',
'tensorflow',
'rag',
'reinforcement-learning',
'transformers',
'prompt-engineering',
'claude',
'gpt',
'vector-search',
'anthropic',
'google-gemini',
'conversational-ai',
'langchain',
'mlops',
'ai-coding',
'ai-safety',
'ai-security',
'embeddings',
'scikit',
'aiops',
'ai-assisted-development',
'keras',
'prompt-injection',
'claude-code',
'llama',
'responsible-ai',
'agentic-ai',
'stable-diffusion',
'huggingface',
'vertex-ai',
'ollama',
'unsupervised-learning',
'bert',
'text-to-speech',
'ai-governance',
'ai-infrastructure',
'text-generation',
'midjourney',
'vibe-coding',
'diffusion-models',
'deepseek',
'mistral-ai',
'grok',
'explainable-ai',
'ai-regulation',
'google-ai',
'vllm',
'spring-ai',
'ai-inference',
'voice-ai',
'stability-ai',
'perplexity',
'weaviate',
'brain-computer-interface',
'microsoft-copilot',
'generative-art',
'azure-openai',
'dalle',
'cohere',
'edge-ai',
'sora',
'amazon-q',
'llmops',
'ai-engineering',
'pinecone',
'gemma',
'local-ai',
'azure-ai',
'windsurf',
'gpt-store',
'codewhisperer',
'duet-ai',
] as const;

export const NO_AI_BLOCKED_WORDS = [
'ChatGPT',
'GPT',
'GPT-4',
'GPT-5',
'OpenAI',
'Claude',
'Claude Code',
'Anthropic',
'Gemini',
'Copilot',
'DeepSeek',
'Mistral',
'LLaMA',
'Llama',
'Ollama',
'Qwen',
'Mixtral',
'Groq',
'Cohere',
'Codex',
'DALL-E',
'Sora',
'Midjourney',
'Stable Diffusion',
'Gemma',
'MiniMax',
'Cursor',
'Windsurf',
'Devin',
'Tabnine',
'Codeium',
'Supermaven',
'LangChain',
'LangSmith',
'HuggingFace',
'Hugging Face',
'vLLM',
'Perplexity',
'Pinecone',
'Weaviate',
'LLM',
'GenAI',
'AGI',
'NLP',
'RAG',
'AI agent',
'AI agents',
'agentic',
'vibe coding',
'vibe-coding',
'machine learning',
'deep learning',
'neural network',
'fine-tuning',
'fine-tune',
'embeddings',
'transformer',
'diffusion model',
'prompt engineering',
'prompt injection',
'inference',
'AI coding',
'AI code',
'AI-generated',
'AI-powered',
'AI-driven',
'AI-assisted',
'AI tool',
'AI tools',
'AI model',
'AI models',
'AI safety',
'AI security',
'AI governance',
'AI regulation',
'Dario Amodei',
'Sam Altman',
'Jensen Huang',
'Elon Musk',
'xAI',
] as const;
18 changes: 18 additions & 0 deletions src/integrations/feed/generators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
baseFeedConfig,
DynamicConfig,
FeedConfigGenerator,
FeedConfigGeneratorResult,
FeedConfigName,
FeedResponse,
FeedVersion,
Expand Down Expand Up @@ -47,6 +48,23 @@ export class FeedGenerator {
extraMetadata,
);
}

withConfigTransform(
transform: (
result: FeedConfigGeneratorResult,
) => FeedConfigGeneratorResult | Promise<FeedConfigGeneratorResult>,
): FeedGenerator {
const baseConfig = this.config;

return new FeedGenerator(
this.client,
{
generate: async (ctx, opts) =>
transform(await baseConfig.generate(ctx, opts)),
},
this.feedId,
);
}
}

const garmFeedService = new GarmrService({
Expand Down
42 changes: 36 additions & 6 deletions src/schema/feeds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ import { randomUUID } from 'crypto';
import { SourceMemberRoles } from '../roles';
import { ContentPreferenceKeyword } from '../entity/contentPreference/ContentPreferenceKeyword';
import { briefingPostIdsMaxItems } from '../common/brief';
import { NO_AI_BLOCKED_TAGS, NO_AI_BLOCKED_WORDS } from '../common/noAiFilter';

interface GQLTagsCategory {
id: string;
Expand Down Expand Up @@ -394,6 +395,11 @@ export const typeDefs = /* GraphQL */ `
Array of supported post types
"""
supportedTypes: [String!]

"""
Exclude AI-related content from the feed
"""
noAi: Boolean = false
): PostConnection! @auth

"""
Expand Down Expand Up @@ -1120,6 +1126,7 @@ interface AnonymousFeedArgs extends FeedArgs {
interface ConfiguredFeedArgs extends FeedArgs {
unreadOnly: boolean;
version: number;
noAi?: boolean;
}

interface SourceFeedArgs extends FeedArgs {
Expand Down Expand Up @@ -1364,6 +1371,27 @@ const anonymousFeedResolverV1: IFieldResolver<
{ allowPrivatePosts: false },
);

const mergeUniqueStrings = (
current: string[] | undefined,
additions: readonly string[],
): string[] => Array.from(new Set([...(current ?? []), ...additions]));

const wrapGeneratorWithNoAi = (generator: FeedGenerator): FeedGenerator =>
generator.withConfigTransform((result) => ({
...result,
config: {
...result.config,
blocked_tags: mergeUniqueStrings(
result.config.blocked_tags,
NO_AI_BLOCKED_TAGS,
),
blocked_title_words: mergeUniqueStrings(
result.config.blocked_title_words,
NO_AI_BLOCKED_WORDS,
),
},
}));

const feedResolverV1: IFieldResolver<unknown, Context, ConfiguredFeedArgs> =
feedResolver(
(
Expand All @@ -1387,10 +1415,8 @@ const feedResolverV1: IFieldResolver<unknown, Context, ConfiguredFeedArgs> =
feedPageGenerator,
applyFeedPaging,
{
fetchQueryParams: async (ctx, args) => {
const feedId = args.feedId || ctx.userId;
return feedToFilters(ctx.con, feedId, ctx.userId);
},
fetchQueryParams: async (ctx, args) =>
feedToFilters(ctx.con, args.feedId || ctx.userId, ctx.userId),
allowPrivatePosts: false,
},
);
Expand Down Expand Up @@ -1567,22 +1593,26 @@ export const resolvers: IResolvers<unknown, BaseContext> = {
},
feed: (source, args: ConfiguredFeedArgs, ctx: Context, info) => {
if (args.version >= 2 && args.ranking === Ranking.POPULARITY) {
const generator = versionToFeedGenerator(args.version);

return feedResolverCursor(
source,
{
...(args as FeedArgs),
generator: versionToFeedGenerator(args.version),
generator: args.noAi ? wrapGeneratorWithNoAi(generator) : generator,
},
ctx,
info,
);
}
if (args.version >= 2 && args.ranking === Ranking.TIME) {
const generator = versionToTimeFeedGenerator(args.version);

return feedResolverCursor(
source,
{
...(args as FeedArgs),
generator: versionToTimeFeedGenerator(args.version),
generator: args.noAi ? wrapGeneratorWithNoAi(generator) : generator,
},
ctx,
info,
Expand Down
Loading